mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
1 Commits
3.0.5-andr
...
fix/error-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd94d9685c |
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.3",
|
||||
"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.5",
|
||||
"version": "3.0.3",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -43,8 +43,9 @@ const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
@@ -116,7 +117,9 @@ export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
@@ -132,8 +135,10 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
const decrypted = await decrypt(
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
@@ -208,7 +213,8 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -232,7 +238,8 @@ export async function deriveCryptoKey(data) {
|
||||
let credentials = await Sodium.deriveKey(data.password, data.salt);
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
|
||||
@@ -34,13 +34,11 @@ import {
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
@@ -150,12 +148,6 @@ const AppLockedOverlay = () => {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -196,7 +188,11 @@ const AppLockedOverlay = () => {
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
if (SettingsService.get().privacyScreen) {
|
||||
|
||||
if (
|
||||
SettingsService.get().privacyScreen ||
|
||||
SettingsService.getProperty("appLockEnabled")
|
||||
) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +209,6 @@ const AppLockedOverlay = () => {
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -44,7 +44,6 @@ type DialogInfo = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -174,7 +174,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
clearAppLockVerificationCipher,
|
||||
setAppLockVerificationCipher,
|
||||
@@ -45,7 +44,6 @@ import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
@@ -68,7 +66,6 @@ export const AppLockPassword = () => {
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [accountPass, setAccountPass] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
@@ -76,7 +73,6 @@ export const AppLockPassword = () => {
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
setMode(mode);
|
||||
setAccountPass(false);
|
||||
setVisible(true);
|
||||
}
|
||||
),
|
||||
@@ -130,9 +126,7 @@ export const AppLockPassword = () => {
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Enter ${
|
||||
accountPass ? "account password" : `app lock ${keyboardType}`
|
||||
} to remove ${keyboardType}`
|
||||
? `Remove app lock ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
icon="shield"
|
||||
@@ -175,39 +169,33 @@ export const AppLockPassword = () => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
accountPass ? null : (
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
)
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
accountPass
|
||||
? "Account password"
|
||||
: mode === "change"
|
||||
mode === "change"
|
||||
? `New ${keyboardType}`
|
||||
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||
}
|
||||
@@ -234,36 +222,6 @@ export const AppLockPassword = () => {
|
||||
placeholder={`Confirm ${keyboardType}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{mode === "remove" ? (
|
||||
<>
|
||||
<Button
|
||||
icon={
|
||||
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
onPress={() => {
|
||||
setSecureTextEntry(false);
|
||||
setAccountPass(!accountPass);
|
||||
setTimeout(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
iconSize={SIZE.lg}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
title="Use account password instead"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
height: 30
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
@@ -291,8 +249,7 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const password = values.current.password;
|
||||
setAppLockVerificationCipher(password);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
@@ -320,6 +277,7 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
@@ -335,10 +293,8 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = values.current.password;
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(password);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
@@ -349,18 +305,14 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(values.current.password)
|
||||
: await validateAppLockPassword(values.current.password);
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.password
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
accountPass
|
||||
? "Account password incorrect"
|
||||
: `${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} incorrect`
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
@@ -390,9 +342,7 @@ export const AppLockPassword = () => {
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle={
|
||||
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
|
||||
}
|
||||
positiveTitle="Save"
|
||||
negativeTitle="Cancel"
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
|
||||
@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
Clipboard.setString(await convertNoteToText(note));
|
||||
ToastManager.show({
|
||||
heading: "Note copied",
|
||||
type: "success",
|
||||
@@ -602,7 +602,7 @@ export class VaultDialog extends Component {
|
||||
await Share.open({
|
||||
heading: "Share note",
|
||||
failOnCancel: false,
|
||||
message: (await convertNoteToText(note)) || ""
|
||||
message: await convertNoteToText(note)
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import Editor from "../../screens/editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
@@ -34,7 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { diff } from "diffblazer";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -121,21 +121,12 @@ export default function NotePreview({ session, content, note }) {
|
||||
<ReadonlyEditor
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
try {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
const currentContent = await db.content.get(_note.contentId);
|
||||
loadContent({
|
||||
data: diff(currentContent.data, content.data),
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(
|
||||
e,
|
||||
"Failed to load history preview",
|
||||
"local"
|
||||
);
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
loadContent({
|
||||
data: content.data,
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -600,11 +600,20 @@ export const useActions = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
const text = await convertNoteToText(item as Note, true);
|
||||
const html = (text || "").replace(/\n/g, "<br />");
|
||||
const text = await convertNoteToText(item as Note, false);
|
||||
if (!text) {
|
||||
ToastManager.error(
|
||||
new Error(Errors.export("text")),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const html = text.replace(/\n/g, "<br />");
|
||||
await Notifications.displayNotification({
|
||||
title: item.title,
|
||||
message: (item as Note).headline || text || "",
|
||||
message: (item as Note).headline || text,
|
||||
subtitle: "",
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
@@ -675,11 +684,15 @@ export const useActions = ({
|
||||
} else {
|
||||
processingId.current = "shareNote";
|
||||
const convertedText = await convertNoteToText(item);
|
||||
if (!convertedText) {
|
||||
ToastManager.error(new Error(Errors.export("text")));
|
||||
return;
|
||||
}
|
||||
processingId.current = undefined;
|
||||
Share.open({
|
||||
title: "Share note to",
|
||||
failOnCancel: false,
|
||||
message: convertedText || ""
|
||||
message: convertedText
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -759,7 +772,11 @@ export const useActions = ({
|
||||
} else {
|
||||
processingId.current = "copyContent";
|
||||
const text = await convertNoteToText(item as Note, true);
|
||||
Clipboard.setString(text || "");
|
||||
if (!text) {
|
||||
ToastManager.error(new Error(Errors.export("text")));
|
||||
return;
|
||||
}
|
||||
Clipboard.setString(text);
|
||||
processingId.current = undefined;
|
||||
ToastManager.show({
|
||||
heading: "Note copied to clipboard",
|
||||
|
||||
@@ -22,14 +22,20 @@ import { Platform } from "react-native";
|
||||
import { Subscription } from "react-native-iap";
|
||||
import PremiumService from "../services/premium";
|
||||
import { db } from "../common/database";
|
||||
import { Product } from "@notesnook/core/dist/api/pricing";
|
||||
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
type PurchaseInfo = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku: string;
|
||||
discount: number;
|
||||
};
|
||||
|
||||
const skuInfos: { [name: string]: PurchaseInfo | undefined } = {};
|
||||
|
||||
export const usePricing = (period: "monthly" | "yearly") => {
|
||||
const [current, setCurrent] = useState<{
|
||||
period: string;
|
||||
info?: Product;
|
||||
info?: PurchaseInfo;
|
||||
product?: Subscription;
|
||||
}>();
|
||||
|
||||
@@ -48,7 +54,6 @@ export const usePricing = (period: "monthly" | "yearly") => {
|
||||
period
|
||||
));
|
||||
skuInfos[period] = skuInfo;
|
||||
|
||||
const products = (await PremiumService.getProducts()) as Subscription[];
|
||||
let product = products.find((p) => p.productId === skuInfo?.sku);
|
||||
if (!product)
|
||||
|
||||
@@ -75,16 +75,14 @@ export function ReadonlyEditor(props: {
|
||||
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
|
||||
console.log("Readonly editor loaded.");
|
||||
props.onLoad?.((content: { data: string; id: string }) => {
|
||||
setTimeout(() => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.getAttachmentData) {
|
||||
const attachment = (editorMessage.value as any).attachment as Attachment;
|
||||
|
||||
@@ -351,18 +351,9 @@ export const useEditor = (
|
||||
if (!unlocked)
|
||||
throw new Error("Could not save note, vault is locked");
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if (typeof noteData.title === "string") {
|
||||
await db.notes.add({
|
||||
title: noteData.title,
|
||||
id: noteData.id
|
||||
});
|
||||
}
|
||||
|
||||
noteData.contentId = note?.contentId;
|
||||
if (data) {
|
||||
await db.vault?.save(noteData as any);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await db.vault?.save(noteData as any);
|
||||
clearTimeout(saveTimer);
|
||||
}
|
||||
|
||||
@@ -705,13 +696,14 @@ export const useEditor = (
|
||||
);
|
||||
}
|
||||
|
||||
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.dateModified) {
|
||||
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -736,7 +728,7 @@ export const useEditor = (
|
||||
}
|
||||
} else {
|
||||
const _nextContent = data.data;
|
||||
if (_nextContent === currentContents.current[note.id]?.data) {
|
||||
if (_nextContent === currentContents.current?.data) {
|
||||
return;
|
||||
}
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
|
||||
@@ -90,26 +90,13 @@ export async function verifyUserWithApplock() {
|
||||
positiveText: "Verify",
|
||||
secureTextEntry: true,
|
||||
negativeText: "Cancel",
|
||||
keyboardType: keyboardType,
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
const verified = await validateAppLockPassword(value);
|
||||
if (!verified) {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(verified);
|
||||
} catch (e) {
|
||||
resolve(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -871,9 +871,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "switch",
|
||||
property: "appLockEnabled",
|
||||
onChange: () => {
|
||||
SettingsService.set({
|
||||
privacyScreen: true
|
||||
});
|
||||
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||
},
|
||||
onVerify: async () => {
|
||||
@@ -976,7 +973,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
SettingsService.getProperty("applockKeyboardType") === "numeric"
|
||||
? "pin"
|
||||
: "password"
|
||||
}, app lock will be disabled if no other security method is enabled.`,
|
||||
}, app lock will fallback to using account password to unlock the app`,
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty("appLockHasPasswordSecurity");
|
||||
},
|
||||
|
||||
@@ -59,14 +59,14 @@ const recoveryKeyMessage = {
|
||||
() => {
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
},
|
||||
false,
|
||||
true,
|
||||
async () => {
|
||||
SettingsService.set({
|
||||
recoveryKeySaved: true
|
||||
});
|
||||
clearMessage();
|
||||
},
|
||||
"Cancel"
|
||||
"I have saved my key already"
|
||||
);
|
||||
},
|
||||
data: {},
|
||||
|
||||
@@ -952,13 +952,13 @@ async function pinNote(id: string) {
|
||||
const note = await db.notes.note(id as string);
|
||||
if (!note) return;
|
||||
|
||||
let text = await convertNoteToText(note, true);
|
||||
let text = await convertNoteToText(note, false);
|
||||
if (!text) text = "";
|
||||
const html = text.replace(/\n/g, "<br />");
|
||||
Notifications.displayNotification({
|
||||
title: note.title,
|
||||
message: note.headline || text,
|
||||
subtitle: "",
|
||||
subtitle: note.headline || text,
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
actions: ["UNPIN"],
|
||||
|
||||
@@ -76,17 +76,6 @@ function migrateAppLock() {
|
||||
DatabaseLogger.debug("App lock Migrated");
|
||||
}
|
||||
|
||||
function migrateSettings(settings: SettingStore["settings"]) {
|
||||
const version = settings.settingsVersion;
|
||||
if (!version) {
|
||||
settings.settingsVersion = 1;
|
||||
settings.privacyScreen = settings.appLockEnabled
|
||||
? true
|
||||
: settings.privacyScreen;
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
scale.fontScale = 1;
|
||||
const settingsJson = MMKV.getString("appSettings");
|
||||
@@ -94,17 +83,14 @@ function init() {
|
||||
if (!settingsJson) {
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
} else {
|
||||
const settingsParsed = JSON.parse(settingsJson);
|
||||
migrateSettings(settingsParsed);
|
||||
settings = {
|
||||
...settings,
|
||||
...settingsParsed
|
||||
...JSON.parse(settingsJson)
|
||||
};
|
||||
}
|
||||
if (settings.fontScale) {
|
||||
scale.fontScale = settings.fontScale;
|
||||
}
|
||||
|
||||
setTimeout(() => setPrivacyScreen(settings), 1);
|
||||
updateSize();
|
||||
useSettingStore.getState().setSettings({ ...settings });
|
||||
@@ -112,7 +98,7 @@ function init() {
|
||||
}
|
||||
|
||||
function setPrivacyScreen(settings: SettingStore["settings"]) {
|
||||
if (settings.privacyScreen) {
|
||||
if (settings.privacyScreen || settings.appLockEnabled) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
} else {
|
||||
|
||||
@@ -81,7 +81,6 @@ export type Settings = {
|
||||
biometricsAuthEnabled?: boolean;
|
||||
backgroundSync?: boolean;
|
||||
applockKeyboardType: "numeric" | "default";
|
||||
settingsVersion?: number;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -170,8 +169,7 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
markdownShortcuts: true,
|
||||
biometricsAuthEnabled: false,
|
||||
appLockHasPasswordSecurity: false,
|
||||
backgroundSync: true,
|
||||
settingsVersion: 0
|
||||
backgroundSync: true
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
@@ -23,7 +23,7 @@ export const IOS_APPGROUPID = "group.org.streetwriters.notesnook";
|
||||
export const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
export const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
|
||||
export const BETA = false;
|
||||
export const BETA = true;
|
||||
|
||||
export const STORE_LINK =
|
||||
Platform.OS === "ios"
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3013
|
||||
versionCode 3011
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- 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 = 2102;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.5;
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
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 = 2102;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.5;
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
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 = 2102;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
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.5;
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
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 = 2102;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
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.5;
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
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 = 2102;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
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.5;
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
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 = 2102;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
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.5;
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -959,4 +959,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.12.1
|
||||
|
||||
4
apps/mobile/package-lock.json
generated
4
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.3",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -311,7 +311,8 @@ const ShareView = () => {
|
||||
type: "tiptap"
|
||||
},
|
||||
id: note.id,
|
||||
sessionId: Date.now()
|
||||
sessionId: Date.now(),
|
||||
title: noteTitle.current
|
||||
};
|
||||
} else {
|
||||
noteData = { ...note };
|
||||
@@ -486,42 +487,25 @@ const ShareView = () => {
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
{appendNoteId ? (
|
||||
<Heading
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontSize: SIZE.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
}}
|
||||
>
|
||||
Save note
|
||||
</Heading>
|
||||
) : (
|
||||
<TextInput
|
||||
placeholder="Enter note title"
|
||||
ref={inputRef}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontSize: SIZE.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
onChangeText={(value) => {
|
||||
noteTitle.current = value;
|
||||
}}
|
||||
defaultValue={noteTitle.current}
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
editorRef.current.focus();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
placeholder="Enter note title"
|
||||
ref={inputRef}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontSize: SIZE.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
}}
|
||||
onChangeText={(value) => {
|
||||
noteTitle.current = value;
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
editorRef.current.focus();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Done"
|
||||
type="accent"
|
||||
|
||||
2
apps/theme-builder/package-lock.json
generated
2
apps/theme-builder/package-lock.json
generated
@@ -978,7 +978,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
@@ -259,15 +259,14 @@ test("unlock a note for editing", async ({ page }) => {
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await page.reload();
|
||||
await notes.waitForList();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const newContent = `${NOTE.content}${content}`;
|
||||
const editedNote = await notes.findNote({
|
||||
title: NOTE.title,
|
||||
content: newContent
|
||||
});
|
||||
if (!editedNote) throw new Error("Could not find note.");
|
||||
await editedNote.openLockedNote(PASSWORD);
|
||||
await editedNote?.openLockedNote(PASSWORD);
|
||||
await notes.editor.waitForLoading();
|
||||
expect(await notes.editor.getContent("text")).toContain(newContent);
|
||||
});
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.3",
|
||||
"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.5",
|
||||
"version": "3.0.3",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -77,19 +77,12 @@ export async function introduceFeatures() {
|
||||
|
||||
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
|
||||
|
||||
export async function createBackup(
|
||||
options: {
|
||||
rescueMode?: boolean;
|
||||
noVerify?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const { rescueMode, noVerify } = options;
|
||||
export async function createBackup(rescueMode = false) {
|
||||
const { isLoggedIn } = useUserStore.getState();
|
||||
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
|
||||
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
|
||||
|
||||
const verified =
|
||||
rescueMode || encryptBackups || noVerify || (await verifyAccount());
|
||||
const verified = rescueMode || encryptBackups || (await verifyAccount());
|
||||
if (!verified) {
|
||||
showToast("error", "Could not create a backup: user verification failed.");
|
||||
return false;
|
||||
|
||||
@@ -18,10 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import hotkeys from "hotkeys-js";
|
||||
import { navigate } from "../navigation";
|
||||
// import { store as themestore } from "../stores/theme-store";
|
||||
import { GlobalKeyboard } from "../utils/keyboard";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { useStore as useSearchStore } from "../stores/search-store";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
|
||||
const KEYMAP = [
|
||||
// {
|
||||
@@ -58,24 +57,11 @@ const KEYMAP = [
|
||||
keys: ["command+f", "ctrl+f"],
|
||||
description: "Search all notes",
|
||||
global: false,
|
||||
action: (e: KeyboardEvent) => {
|
||||
const isInEditor =
|
||||
e.target instanceof HTMLElement &&
|
||||
!!e.target?.closest(".editor-container");
|
||||
if (isInEditor) {
|
||||
const activeSession = useEditorStore.getState().getActiveSession();
|
||||
if (activeSession?.type === "readonly") {
|
||||
e.preventDefault();
|
||||
const editor = useEditorManager
|
||||
.getState()
|
||||
.getEditor(activeSession.id);
|
||||
editor?.editor?.startSearch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
action: (e) => {
|
||||
if (e.target?.classList.contains("ProseMirror")) return;
|
||||
e.preventDefault();
|
||||
|
||||
useSearchStore.setState({ isSearching: true, searchType: "notes" });
|
||||
navigate("/search/notes");
|
||||
}
|
||||
}
|
||||
// {
|
||||
@@ -151,11 +137,7 @@ export function registerKeyMap() {
|
||||
KEYMAP.forEach((key) => {
|
||||
hotkeys(
|
||||
key.keys.join(","),
|
||||
{
|
||||
element: key.global
|
||||
? (GlobalKeyboard as unknown as HTMLElement)
|
||||
: document.body
|
||||
},
|
||||
{ element: key.global ? GlobalKeyboard : window },
|
||||
key.action
|
||||
);
|
||||
});
|
||||
@@ -177,7 +177,7 @@ function isIgnored(key: keyof typeof NoticesData) {
|
||||
let openedToast: { hide: () => void } | null = null;
|
||||
async function saveBackup() {
|
||||
if (IS_DESKTOP_APP) {
|
||||
await createBackup({ noVerify: true });
|
||||
await createBackup();
|
||||
} else if (isUserPremium() && !IS_TESTING) {
|
||||
if (openedToast !== null) return;
|
||||
openedToast = showToast(
|
||||
|
||||
@@ -83,7 +83,7 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
}
|
||||
|
||||
activate(
|
||||
portProviderFunc: () => Promise<{ port: MessagePort; onclose: () => void }>,
|
||||
portProviderFunc: () => MessagePort | Promise<MessagePort>,
|
||||
onClientConnected: () => Promise<void>
|
||||
) {
|
||||
if (this.#onDeactivate) return;
|
||||
@@ -97,7 +97,7 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
navigator.locks
|
||||
.request(LOCK_NAME, { signal: this.#onDeactivate.signal }, async () => {
|
||||
// Get the port to request client ports.
|
||||
const { port, onclose } = await portProviderFunc();
|
||||
const port = await portProviderFunc();
|
||||
port.start();
|
||||
|
||||
// Listen for client requests. A separate BroadcastChannel
|
||||
@@ -159,7 +159,6 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
// Release the lock only on user abort or context destruction.
|
||||
return new Promise((_, reject) => {
|
||||
this.#onDeactivate?.signal.addEventListener("abort", () => {
|
||||
onclose();
|
||||
broadcastChannel.close();
|
||||
reject(this.#onDeactivate?.signal.reason);
|
||||
});
|
||||
|
||||
@@ -233,8 +233,6 @@ addEventListener("message", async (event) => {
|
||||
await worker.open(event.data.dbName, event.data.async, event.data.uri);
|
||||
const providerPort = createSharedServicePort(worker);
|
||||
postMessage(null, [providerPort]);
|
||||
|
||||
self.addEventListener("beforeunload", () => worker.close());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -72,18 +72,14 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
|
||||
service.activate(
|
||||
() =>
|
||||
new Promise<{ port: MessagePort; onclose: () => void }>((resolve) => {
|
||||
new Promise<MessagePort>((resolve) => {
|
||||
console.log("initializing worker");
|
||||
this.needsInitialization = true;
|
||||
|
||||
const worker = new Worker();
|
||||
worker.addEventListener(
|
||||
"message",
|
||||
(event) =>
|
||||
resolve({
|
||||
port: event.ports[0],
|
||||
onclose: () => worker.terminate()
|
||||
}),
|
||||
(event) => resolve(event.ports[0]),
|
||||
{ once: true }
|
||||
);
|
||||
worker.postMessage({
|
||||
|
||||
@@ -307,7 +307,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
|
||||
break;
|
||||
}
|
||||
case "backup": {
|
||||
await createBackup();
|
||||
await createBackup(true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -331,6 +331,12 @@ async function resolveConflict({
|
||||
toKeepDateEdited: number;
|
||||
dateResolved?: number;
|
||||
}) {
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
dateEdited: toKeepDateEdited,
|
||||
conflicted: false
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: toKeep,
|
||||
@@ -339,17 +345,12 @@ async function resolveConflict({
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
dateEdited: toKeepDateEdited,
|
||||
conflicted: false
|
||||
});
|
||||
|
||||
if (toCopy) {
|
||||
await createCopy(note, toCopy);
|
||||
}
|
||||
|
||||
await notesStore.refresh();
|
||||
useEditorStore.getState().openSession(note.id, { force: true });
|
||||
}
|
||||
|
||||
async function createCopy(note: Note, content: ContentItem) {
|
||||
|
||||
@@ -152,6 +152,7 @@ export function EditorActionBar() {
|
||||
activeSession &&
|
||||
activeSession.type !== "new" &&
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "readonly" &&
|
||||
activeSession.type !== "diff" &&
|
||||
activeSession.type !== "conflicted",
|
||||
onClick: editor?.startSearch
|
||||
|
||||
@@ -271,7 +271,7 @@ function EditorView({
|
||||
const isContent =
|
||||
item.type === "tiptap" && item.noteId === session.note.id;
|
||||
const isNote = item.type === "note" && item.id === session.note.id;
|
||||
if (isContent && lastChangedTime.current < item.dateModified) {
|
||||
if (isContent && lastChangedTime.current < item.dateEdited) {
|
||||
if (!item.locked) return editor.updateContent(item.data);
|
||||
|
||||
const result = await db.vault
|
||||
|
||||
@@ -344,6 +344,7 @@ function TipTap(props: TipTapProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (readonly) return null;
|
||||
return (
|
||||
<>
|
||||
<ScopedThemeProvider
|
||||
@@ -417,7 +418,7 @@ function TiptapWrapper(
|
||||
editorContainer={() => {
|
||||
if (editorContainerRef.current) return editorContainerRef.current;
|
||||
const editorContainer = document.createElement("div");
|
||||
editorContainer.classList.add("selectable", "editor-container");
|
||||
editorContainer.classList.add("selectable");
|
||||
editorContainer.style.flex = "1";
|
||||
editorContainer.style.cursor = "text";
|
||||
editorContainer.style.color =
|
||||
@@ -426,7 +427,6 @@ function TiptapWrapper(
|
||||
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
|
||||
editorContainer.style.fontFamily =
|
||||
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
|
||||
editorContainer.tabIndex = -1;
|
||||
editorContainerRef.current = editorContainer;
|
||||
return editorContainer;
|
||||
}}
|
||||
|
||||
@@ -162,10 +162,9 @@ function getErrorHelp(props: FallbackProps) {
|
||||
if (
|
||||
errorText.includes("file is not a database") ||
|
||||
errorText.includes("unsupported file format") ||
|
||||
errorText.includes("database disk image is malformed") ||
|
||||
errorText.includes("null function or function signature mismatch") ||
|
||||
errorText.includes("malformed database schema") ||
|
||||
/table ".+?" already exists/.test(errorText) ||
|
||||
errorText.includes("corrupted migrations:")
|
||||
errorText.includes("malformed database schema")
|
||||
) {
|
||||
return {
|
||||
explanation: `This error usually means the database file is either corrupt or it could not be decrypted.`,
|
||||
@@ -196,17 +195,6 @@ function getErrorHelp(props: FallbackProps) {
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
} else if (errorText.includes("database disk image is malformed")) {
|
||||
return {
|
||||
explanation: `This error usually means the search index is corrupted.`,
|
||||
action:
|
||||
"This error can be fixed by rebuilding the search index. This action won't result in any kind of data loss.",
|
||||
fix: async () => {
|
||||
const { db } = await import("../../common/db");
|
||||
await db.lookup.rebuild();
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -524,6 +524,8 @@ const menuItems: (
|
||||
type: "button",
|
||||
key: "duplicate",
|
||||
title: "Duplicate",
|
||||
//!isSynced ||
|
||||
isDisabled: context?.locked,
|
||||
icon: Duplicate.path,
|
||||
onClick: () => store.get().duplicate(...ids),
|
||||
multiSelect: true
|
||||
|
||||
@@ -233,15 +233,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (noteId && session.id !== noteId && session.note.id !== noteId)
|
||||
continue;
|
||||
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
|
||||
// if a note becomes conflicted, reopen the session
|
||||
else if (
|
||||
session.type !== "conflicted" &&
|
||||
item.type === "tiptap" &&
|
||||
item.conflicted
|
||||
)
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
// if a note is locked, reopen the session
|
||||
else if (
|
||||
session.type === "default" &&
|
||||
@@ -266,7 +257,9 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
}
|
||||
// if a deleted note is restored, reopen the session
|
||||
else if (session.type === "deleted" && item.type === "note") {
|
||||
openSession(session.note.id, { force: true, silent: true });
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
}
|
||||
// if a readonly note is made editable, reopen the session
|
||||
else if (
|
||||
@@ -274,7 +267,9 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
item.type === "note" &&
|
||||
!item.readonly
|
||||
)
|
||||
openSession(session.note.id, { force: true, silent: true });
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
// update the note in all sessions
|
||||
else if (item.type === "note") {
|
||||
updateSession(
|
||||
@@ -349,10 +344,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
session.type !== "new" && session.note.contentId;
|
||||
if (!contentId || !event.ids.includes(contentId)) continue;
|
||||
if (
|
||||
// if note's conflict is resolved
|
||||
(session.type === "conflicted" && !event.item.conflicted) ||
|
||||
// if note becomes conflicted
|
||||
(session.type !== "conflicted" && event.item.conflicted) ||
|
||||
// if note is locked
|
||||
(session.type === "default" &&
|
||||
!session.locked &&
|
||||
@@ -557,14 +548,12 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
|
||||
const session = getSession(noteId);
|
||||
|
||||
if (session && !options.force) {
|
||||
if (!session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
if (session && !options.force && !session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
|
||||
if (session.type === "diff" || session.type === "conflicted") {
|
||||
return openDiffSession(session.note.id, session.id);
|
||||
}
|
||||
if (session && (session.type === "diff" || session.type === "conflicted")) {
|
||||
return openDiffSession(session.note.id, session.id);
|
||||
}
|
||||
|
||||
if (session && session.id) await db.fs().cancel(session.id);
|
||||
@@ -577,7 +566,19 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const isPreview = session ? session.preview : !options?.newSession;
|
||||
const isLocked = await db.vaults.itemExists(note);
|
||||
|
||||
if (note.conflicted) {
|
||||
if (isLocked && note.type !== "trash") {
|
||||
this.addSession(
|
||||
{
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
pinned: session?.pinned,
|
||||
note,
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else if (note.conflicted) {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
@@ -611,18 +612,6 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else if (isLocked && note.type !== "trash") {
|
||||
this.addSession(
|
||||
{
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
pinned: session?.pinned,
|
||||
note,
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
|
||||
@@ -458,7 +458,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
|
||||
"Please wait while we create a backup file for you to download."
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
await createBackup({ rescueMode: true });
|
||||
await createBackup(true);
|
||||
navigate("new");
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -56,7 +56,6 @@ export default defineConfig({
|
||||
minify: "esbuild",
|
||||
cssMinify: true,
|
||||
emptyOutDir: true,
|
||||
sourcemap: !isDesktop,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
plugins: [emitEditorStyles()],
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1,3 +0,0 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -28,7 +28,7 @@ import { login } from "./utils";
|
||||
import { SqliteDialect } from "kysely";
|
||||
import BetterSQLite3 from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
const TEST_TIMEOUT = 60 * 1000;
|
||||
const TEST_TIMEOUT = 30 * 1000;
|
||||
|
||||
test(
|
||||
"case 1: device A & B should only download the changes from device C (no uploading)",
|
||||
@@ -156,111 +156,6 @@ test(
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
test(
|
||||
"issue: syncing should not affect the items' dateModified",
|
||||
async (t) => {
|
||||
const [deviceA] = await Promise.all([initializeDevice("deviceA")]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
const noteDateBefore = (await deviceA.notes.note(noteId)).dateModified;
|
||||
const contentDateBefore = (await deviceA.content.findByNoteId(noteId))
|
||||
.dateModified;
|
||||
await deviceA.sync({ type: "full" });
|
||||
const noteDateAfter = (await deviceA.notes.note(noteId)).dateModified;
|
||||
const contentDateAfter = (await deviceA.content.findByNoteId(noteId))
|
||||
.dateModified;
|
||||
expect(noteDateBefore).toBe(noteDateAfter);
|
||||
expect(contentDateBefore).toBe(contentDateAfter);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"case 4: local content changed after remote content should create a conflict",
|
||||
async (t) => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA, deviceB);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await deviceB.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
await deviceA.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
|
||||
expect(await deviceA.notes.conflicted.count()).toBeGreaterThan(0);
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
test(
|
||||
"case 5: remote content changed after local content should create a conflict",
|
||||
async (t) => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA, deviceB);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await deviceA.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
await deviceB.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
expect(await deviceB.notes.conflicted.count()).toBeGreaterThan(0);
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
// test(
|
||||
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
|
||||
// async () => {
|
||||
@@ -483,8 +378,6 @@ test(
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
async function initializeDevice(id, capabilities = []) {
|
||||
// initialize(new NodeStorageInterface(), false);
|
||||
|
||||
console.time(`Init ${id}`);
|
||||
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
|
||||
return {
|
||||
|
||||
@@ -179,7 +179,7 @@ class Database {
|
||||
subscriptions = new Subscriptions(this.tokenManager);
|
||||
offers = new Offers();
|
||||
debug = new Debug();
|
||||
pricing = Pricing;
|
||||
pricing = new Pricing();
|
||||
|
||||
user = new UserManager(this);
|
||||
syncer = new Sync(this);
|
||||
|
||||
@@ -92,10 +92,7 @@ class MFAManager {
|
||||
}
|
||||
|
||||
async sendCode(method: "sms" | "email") {
|
||||
const token = await this.tokenManager.getAccessToken([
|
||||
"IdentityServerApi",
|
||||
"auth:grant_types:mfa"
|
||||
]);
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
if (!token) throw new Error("Unauthorized.");
|
||||
|
||||
return await http.post(
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import http from "../utils/http";
|
||||
|
||||
export type Product = {
|
||||
type Product = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku?: string;
|
||||
|
||||
@@ -123,7 +123,7 @@ describe.concurrent("merge content", (test) => {
|
||||
await loginFakeUser(db);
|
||||
const merger = new Merger(db);
|
||||
|
||||
const merged = merger.mergeContent(
|
||||
const merged = await merger.mergeContent(
|
||||
{
|
||||
type: "tiptap",
|
||||
data: "Remote",
|
||||
@@ -132,7 +132,6 @@ describe.concurrent("merge content", (test) => {
|
||||
{
|
||||
type: "tiptap",
|
||||
data: "Local",
|
||||
synced: true,
|
||||
dateEdited: Date.now() - 1000
|
||||
}
|
||||
);
|
||||
@@ -141,13 +140,13 @@ describe.concurrent("merge content", (test) => {
|
||||
expect(merged.data).toBe("Remote");
|
||||
}));
|
||||
|
||||
test("trigger conflict if local item is unsynced", () =>
|
||||
test("trigger conflict if local item dateEdited is newer", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
await loginFakeUser(db);
|
||||
const merger = new Merger(db);
|
||||
|
||||
const noteId = await db.notes.add(TEST_NOTE);
|
||||
const merged = merger.mergeContent(
|
||||
const merged = await merger.mergeContent(
|
||||
{
|
||||
type: "tiptap",
|
||||
data: "Remote",
|
||||
@@ -158,7 +157,7 @@ describe.concurrent("merge content", (test) => {
|
||||
type: "tiptap",
|
||||
data: "Local",
|
||||
noteId,
|
||||
synced: false,
|
||||
|
||||
dateEdited: Date.now()
|
||||
}
|
||||
);
|
||||
@@ -167,6 +166,7 @@ describe.concurrent("merge content", (test) => {
|
||||
expect(merged.data).toBe("Local");
|
||||
expect(merged.conflicted).toBeDefined();
|
||||
expect(merged.conflicted.data).toBe("Remote");
|
||||
expect(await db.notes.conflicted.has(noteId)).toBe(true);
|
||||
}));
|
||||
|
||||
test("merge conflicts if local item is already conflicted", () =>
|
||||
|
||||
@@ -57,34 +57,37 @@ class Collector {
|
||||
const collection = this.db[collectionKey].collection;
|
||||
let pushTimestamp = Date.now();
|
||||
for await (const chunk of collection.unsynced(chunkSize, isForceSync)) {
|
||||
const { ids, items: syncableItems } = filterSyncableItems(chunk);
|
||||
if (!ids.length) continue;
|
||||
const ciphers = await this.db
|
||||
.storage()
|
||||
.encryptMulti(key, syncableItems);
|
||||
const items = toPushItem(ids, ciphers);
|
||||
const items = await this.prepareChunk(chunk, key);
|
||||
if (!items) continue;
|
||||
yield { items, type: itemType };
|
||||
|
||||
await this.db
|
||||
.sql()
|
||||
.updateTable(collection.type)
|
||||
.where("id", "in", ids)
|
||||
// EDGE CASE:
|
||||
// Sometimes an item can get updated while it's being pushed.
|
||||
// The result is that its `synced` property becomes true even
|
||||
// though it's modification wasn't yet synced.
|
||||
// In order to prevent that, we only set the `synced` property
|
||||
// to true for items that haven't been modified since we last ran
|
||||
// the push. Everything else will be collected again in the next
|
||||
// push.
|
||||
.where("dateModified", "<=", pushTimestamp)
|
||||
.set({ synced: true })
|
||||
.execute();
|
||||
await collection.update(
|
||||
chunk.map((i) => i.id),
|
||||
{ synced: true },
|
||||
{
|
||||
sendEvent: false,
|
||||
// EDGE CASE:
|
||||
// Sometimes an item can get updated while it's being pushed.
|
||||
// The result is that its `synced` property becomes true even
|
||||
// though it's modification wasn't yet synced.
|
||||
// In order to prevent that, we only set the `synced` property
|
||||
// to true for items that haven't been modified since we last ran
|
||||
// the push. Everything else will be collected again in the next
|
||||
// push.
|
||||
condition: (eb) => eb("dateModified", "<=", pushTimestamp)
|
||||
}
|
||||
);
|
||||
pushTimestamp = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async prepareChunk(chunk: MaybeDeletedItem<Item>[], key: SerializedKey) {
|
||||
const { ids, items } = filterSyncableItems(chunk);
|
||||
if (!ids.length) return;
|
||||
const ciphers = await this.db.storage().encryptMulti(key, items);
|
||||
return toPushItem(ids, ciphers);
|
||||
}
|
||||
}
|
||||
export default Collector;
|
||||
|
||||
@@ -110,6 +113,9 @@ function filterSyncableItems(items: MaybeDeletedItem<Item>[]): {
|
||||
const ids = [];
|
||||
const syncableItems = [];
|
||||
for (const item of items) {
|
||||
// do not sync conflicted note or content
|
||||
if ("conflicted" in item && item.conflicted) continue;
|
||||
|
||||
delete item.synced;
|
||||
|
||||
ids.push(item.id);
|
||||
|
||||
@@ -242,20 +242,6 @@ class Sync {
|
||||
|
||||
this.connection?.off("SendItems");
|
||||
this.connection?.off("SendVaultKey");
|
||||
|
||||
await this.db
|
||||
.sql()
|
||||
.updateTable("notes")
|
||||
.where("id", "in", (eb) =>
|
||||
eb
|
||||
.selectFrom("content")
|
||||
.select("noteId as id")
|
||||
.where("conflicted", "is not", null)
|
||||
.where("conflicted", "is not", false)
|
||||
.$castTo<string | null>()
|
||||
)
|
||||
.set({ conflicted: true })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async send(deviceId: string, isForceSync?: boolean) {
|
||||
@@ -348,8 +334,10 @@ class Sync {
|
||||
const localItems = await collection.records(chunk.items.map((i) => i.id));
|
||||
let items: (MaybeDeletedItem<Item> | undefined)[] = [];
|
||||
if (itemType === "content") {
|
||||
items = deserialized.map((item) =>
|
||||
this.merger.mergeContent(item, localItems[item.id])
|
||||
items = await Promise.all(
|
||||
deserialized.map((item) =>
|
||||
this.merger.mergeContent(item, localItems[item.id])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
items =
|
||||
|
||||
@@ -46,7 +46,7 @@ class Merger {
|
||||
}
|
||||
}
|
||||
|
||||
mergeContent(
|
||||
async mergeContent(
|
||||
remoteItem: MaybeDeletedItem<Item>,
|
||||
localItem: MaybeDeletedItem<Item> | undefined
|
||||
) {
|
||||
@@ -75,7 +75,10 @@ class Merger {
|
||||
else if (!conflicted) return;
|
||||
|
||||
// otherwise we trigger the conflicts
|
||||
this.logger.info("conflict marked", { id: localItem.noteId });
|
||||
await this.db.notes.add({
|
||||
id: localItem.noteId,
|
||||
conflicted: true
|
||||
});
|
||||
localItem.conflicted = remoteItem;
|
||||
return localItem;
|
||||
}
|
||||
@@ -121,8 +124,9 @@ export function isContentConflicted(
|
||||
remoteItem.dateModified &&
|
||||
localItem.dateResolved === remoteItem.dateModified;
|
||||
const isEdited =
|
||||
// the local item is edited if it wasn't synced yet.
|
||||
!localItem.synced;
|
||||
// the local item is edited if it was changed/edited after the remote
|
||||
// note and it also wasn't synced yet.
|
||||
localItem.dateEdited > remoteItem.dateEdited && !localItem.synced;
|
||||
if (isEdited && !isResolved) {
|
||||
// If time difference between local item's edits & remote item's edits
|
||||
// is less than threshold, we shouldn't trigger a merge conflict; instead
|
||||
|
||||
@@ -32,15 +32,6 @@ export type Token = {
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
type Scope = (typeof SCOPES)[number];
|
||||
|
||||
const SCOPES = [
|
||||
"notesnook.sync",
|
||||
"offline_access",
|
||||
"IdentityServerApi",
|
||||
"auth:grant_types:mfa",
|
||||
"auth:grant_types:mfa_password"
|
||||
] as const;
|
||||
const ENDPOINTS = {
|
||||
token: "/connect/token",
|
||||
revoke: "/connect/revocation",
|
||||
@@ -88,14 +79,10 @@ class TokenManager {
|
||||
return scopes.includes("offline_access") && Boolean(refresh_token);
|
||||
}
|
||||
|
||||
async getAccessToken(
|
||||
scopes: Scope[] = ["notesnook.sync", "IdentityServerApi"],
|
||||
forceRenew = false
|
||||
) {
|
||||
async getAccessToken(forceRenew = false) {
|
||||
return await getSafeToken(async () => {
|
||||
const token = await this.getToken(true, forceRenew);
|
||||
if (!token || !token.scope) return;
|
||||
if (!scopes.some((s) => token.scope.includes(s))) return;
|
||||
if (!token || token.scope.includes("auth:grant_types")) return;
|
||||
return token.access_token;
|
||||
}, "Error getting access token:");
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ class UserManager {
|
||||
username: email,
|
||||
password: hashedPassword,
|
||||
grant_type: code ? "mfa" : "password",
|
||||
scope: "notesnook.sync offline_access IdentityServerApi",
|
||||
scope: "notesnook.sync offline_access openid IdentityServerApi",
|
||||
client_id: "notesnook",
|
||||
"mfa:code": code,
|
||||
"mfa:method": method
|
||||
|
||||
@@ -175,7 +175,10 @@ export class Content implements ICollection {
|
||||
const content = await this.collection.get(id);
|
||||
if (!content || isDeleted(content)) return;
|
||||
if (!content.locked && this.preProcess(content)) {
|
||||
await this.collection.update([content.id], content, { modify: false });
|
||||
await this.db.content.add({
|
||||
...content,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@@ -243,7 +246,10 @@ export class Content implements ICollection {
|
||||
.executeTakeFirst()) as ContentItem;
|
||||
if (!content || isDeleted(content)) return;
|
||||
if (!content.locked && this.preProcess(content)) {
|
||||
await this.collection.update([content.id], content, { modify: false });
|
||||
await this.db.content.add({
|
||||
...content,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -318,9 +318,17 @@ export class Notes implements ICollection {
|
||||
const content = note.contentId
|
||||
? await this.db.content.get(note.contentId)
|
||||
: undefined;
|
||||
if (content && (isDeleted(content) || content.locked))
|
||||
throw new Error("Cannot duplicate a locked or deleted note.");
|
||||
const duplicateId = await this.db.notes.add({
|
||||
...clone(note),
|
||||
id: undefined,
|
||||
content: content
|
||||
? {
|
||||
type: content.type,
|
||||
data: content.data
|
||||
}
|
||||
: undefined,
|
||||
readonly: false,
|
||||
favorite: false,
|
||||
pinned: false,
|
||||
@@ -330,38 +338,19 @@ export class Notes implements ICollection {
|
||||
dateCreated: undefined,
|
||||
dateModified: undefined
|
||||
});
|
||||
if (!duplicateId) continue;
|
||||
|
||||
const contentId = await this.db.content.add({
|
||||
...clone(content),
|
||||
id: undefined,
|
||||
noteId: duplicateId,
|
||||
dateResolved: undefined,
|
||||
dateEdited: undefined,
|
||||
dateCreated: undefined,
|
||||
dateModified: undefined
|
||||
});
|
||||
|
||||
await this.db.notes.add({ id: duplicateId, contentId });
|
||||
|
||||
for (const relation of await this.db.relations.to(note).get()) {
|
||||
for (const relation of await this.db.relations
|
||||
.to(note, "notebook")
|
||||
.get()) {
|
||||
await this.db.relations.add(
|
||||
{ type: relation.fromType, id: relation.fromId },
|
||||
{ type: "notebook", id: relation.fromId },
|
||||
{
|
||||
id: duplicateId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const relation of await this.db.relations.from(note).get()) {
|
||||
await this.db.relations.add(
|
||||
{
|
||||
id: duplicateId,
|
||||
type: "note"
|
||||
},
|
||||
{ type: relation.toType, id: relation.toId }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,6 @@ export class Relations implements ICollection {
|
||||
});
|
||||
}
|
||||
|
||||
from(
|
||||
reference: ItemReference | ItemReferences
|
||||
): RelationsArray<keyof RelatableTable>;
|
||||
from(
|
||||
reference: ItemReference | ItemReferences,
|
||||
types: (keyof RelatableTable)[]
|
||||
@@ -76,19 +73,16 @@ export class Relations implements ICollection {
|
||||
): RelationsArray<TType>;
|
||||
from<TType extends keyof RelatableTable = keyof RelatableTable>(
|
||||
reference: ItemReference | ItemReferences,
|
||||
type?: TType | keyof RelatableTable[]
|
||||
type: TType | keyof RelatableTable[]
|
||||
) {
|
||||
return new RelationsArray(
|
||||
this.db,
|
||||
reference,
|
||||
type ? (Array.isArray(type) ? type : ([type] as TType[])) : undefined,
|
||||
Array.isArray(type) ? type : [type],
|
||||
"from"
|
||||
);
|
||||
}
|
||||
|
||||
to(
|
||||
reference: ItemReference | ItemReferences
|
||||
): RelationsArray<keyof RelatableTable>;
|
||||
to(
|
||||
reference: ItemReference | ItemReferences,
|
||||
types: (keyof RelatableTable)[]
|
||||
@@ -99,12 +93,12 @@ export class Relations implements ICollection {
|
||||
): RelationsArray<TType>;
|
||||
to<TType extends keyof RelatableTable = keyof RelatableTable>(
|
||||
reference: ItemReference | ItemReferences,
|
||||
type?: TType | keyof RelatableTable[]
|
||||
type: TType | keyof RelatableTable[]
|
||||
) {
|
||||
return new RelationsArray(
|
||||
this.db,
|
||||
reference,
|
||||
type ? (Array.isArray(type) ? type : ([type] as TType[])) : undefined,
|
||||
Array.isArray(type) ? type : [type],
|
||||
"to"
|
||||
);
|
||||
}
|
||||
@@ -230,26 +224,21 @@ const TABLE_MAP = {
|
||||
type RelatableTable = typeof TABLE_MAP;
|
||||
|
||||
class RelationsArray<TType extends keyof RelatableTable> {
|
||||
private table: ValueOf<RelatableTable> = TABLE_MAP[this.types[0]];
|
||||
|
||||
constructor(
|
||||
private readonly db: Database,
|
||||
private readonly reference: ItemReference | ItemReferences,
|
||||
private readonly types: TType[] | undefined,
|
||||
private readonly types: TType[],
|
||||
private readonly direction: "from" | "to"
|
||||
) {}
|
||||
|
||||
get selector() {
|
||||
if (!this.types)
|
||||
throw new Error("Cannot use selector when no tables are specified.");
|
||||
if (this.types.length > 1)
|
||||
throw new Error(
|
||||
"Cannot use selector when more than 1 tables are specified."
|
||||
);
|
||||
const table: ValueOf<RelatableTable> = TABLE_MAP[this.types[0]];
|
||||
return new FilteredSelector<ItemMap[TType]>(
|
||||
table,
|
||||
this.table,
|
||||
this.db
|
||||
.sql()
|
||||
.selectFrom<keyof DatabaseSchema>(table)
|
||||
.selectFrom<keyof DatabaseSchema>(this.table)
|
||||
.where("id", "in", (b) =>
|
||||
b
|
||||
.selectFrom("relations")
|
||||
@@ -367,12 +356,10 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
) => {
|
||||
if (this.direction === "to") {
|
||||
return builder
|
||||
.$if(!!this.types, (eb) =>
|
||||
eb.where(
|
||||
"fromType",
|
||||
this.types!.length > 1 ? "in" : "==",
|
||||
this.types!.length > 1 ? this.types : this.types![0]
|
||||
)
|
||||
.where(
|
||||
"fromType",
|
||||
this.types.length > 1 ? "in" : "==",
|
||||
this.types.length > 1 ? this.types : this.types[0]
|
||||
)
|
||||
.where("toType", "==", this.reference.type)
|
||||
.where(
|
||||
@@ -383,12 +370,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
: this.reference.id
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("note" as TType) &&
|
||||
this.types.includes("note" as TType) &&
|
||||
this.db.trash.cache.notes.length > 0,
|
||||
(b) => b.where("fromId", "not in", this.db.trash.cache.notes)
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("notebook" as TType) &&
|
||||
this.types.includes("notebook" as TType) &&
|
||||
this.db.trash.cache.notebooks.length > 0,
|
||||
(b) => b.where("fromId", "not in", this.db.trash.cache.notebooks)
|
||||
)
|
||||
@@ -396,12 +383,10 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
.$narrowType<{ id: string }>();
|
||||
} else {
|
||||
return builder
|
||||
.$if(!!this.types, (eb) =>
|
||||
eb.where(
|
||||
"toType",
|
||||
this.types!.length > 1 ? "in" : "==",
|
||||
this.types!.length > 1 ? this.types : this.types![0]
|
||||
)
|
||||
.where(
|
||||
"toType",
|
||||
this.types.length > 1 ? "in" : "==",
|
||||
this.types.length > 1 ? this.types : this.types[0]
|
||||
)
|
||||
.where("fromType", "==", this.reference.type)
|
||||
.where(
|
||||
@@ -412,12 +397,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
: this.reference.id
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("note" as TType) &&
|
||||
this.types.includes("note" as TType) &&
|
||||
this.db.trash.cache.notes.length > 0,
|
||||
(b) => b.where("toId", "not in", this.db.trash.cache.notes)
|
||||
)
|
||||
.$if(
|
||||
!!this.types?.includes("notebook" as TType) &&
|
||||
this.types.includes("notebook" as TType) &&
|
||||
this.db.trash.cache.notebooks.length > 0,
|
||||
(b) => b.where("toId", "not in", this.db.trash.cache.notebooks)
|
||||
)
|
||||
|
||||
@@ -184,7 +184,7 @@ export class Tiptap {
|
||||
this.data = new HTMLRewriter({
|
||||
ontag: (name, attr) => {
|
||||
const href = attr[ATTRIBUTES.href];
|
||||
if (name === "a" && href && href.startsWith("nn://")) {
|
||||
if (name === "a" && href.startsWith("nn://")) {
|
||||
const link = resolve(href);
|
||||
if (!link) return;
|
||||
attr[ATTRIBUTES.href] = link;
|
||||
@@ -244,10 +244,7 @@ export class Tiptap {
|
||||
if (types.includes("internalLinks")) {
|
||||
result.internalLinks.push(
|
||||
...findAll(
|
||||
(e) =>
|
||||
e.tagName === "a" &&
|
||||
!!e.attribs.href &&
|
||||
e.attribs.href.startsWith("nn://"),
|
||||
(e) => e.tagName === "a" && e.attribs.href.startsWith("nn://"),
|
||||
document.childNodes
|
||||
)
|
||||
.map((e) => parseInternalLink(e.attribs.href))
|
||||
|
||||
@@ -160,7 +160,6 @@ type AsyncOrSyncResult<Async extends boolean, Response> = Async extends true
|
||||
: Response;
|
||||
|
||||
export interface DatabaseCollection<T, IsAsync extends boolean> {
|
||||
type: keyof DatabaseSchema;
|
||||
clear(): Promise<void>;
|
||||
init(): Promise<void>;
|
||||
upsert(item: T): Promise<void>;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class SQLCachedCollection<
|
||||
startTransaction: (
|
||||
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
|
||||
) => Promise<void>,
|
||||
public type: TCollectionType,
|
||||
type: TCollectionType,
|
||||
eventManager: EventManager,
|
||||
sanitizer: Sanitizer
|
||||
) {
|
||||
|
||||
@@ -75,7 +75,7 @@ export class SQLCollection<
|
||||
_startTransaction: (
|
||||
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
|
||||
) => Promise<void>,
|
||||
public readonly type: TCollectionType,
|
||||
private readonly type: TCollectionType,
|
||||
private readonly eventManager: EventManager,
|
||||
private readonly sanitizer: Sanitizer
|
||||
) {}
|
||||
@@ -227,16 +227,14 @@ export class SQLCollection<
|
||||
ids: string[],
|
||||
partial: Partial<SQLiteItem<T>>,
|
||||
options: {
|
||||
sendEvent?: boolean;
|
||||
modify?: boolean;
|
||||
sendEvent: boolean;
|
||||
condition?: ExpressionOrFactory<
|
||||
DatabaseSchema,
|
||||
keyof DatabaseSchema,
|
||||
SqlBool
|
||||
>;
|
||||
} = {}
|
||||
} = { sendEvent: true }
|
||||
) {
|
||||
const { sendEvent = true, modify = true, condition } = options;
|
||||
if (!this.sanitizer.sanitize(this.type, partial)) return;
|
||||
|
||||
await this.db()
|
||||
@@ -246,16 +244,16 @@ export class SQLCollection<
|
||||
await tx
|
||||
.updateTable<keyof DatabaseSchema>(this.type)
|
||||
.where("id", "in", chunk)
|
||||
.$if(!!condition, (eb) => eb.where(condition!))
|
||||
.$if(!!options.condition, (eb) => eb.where(options.condition!))
|
||||
.set({
|
||||
...partial,
|
||||
dateModified: modify ? Date.now() : undefined,
|
||||
dateModified: Date.now(),
|
||||
synced: partial.synced || false
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
if (sendEvent) {
|
||||
if (options.sendEvent) {
|
||||
this.eventManager.publish(EVENTS.databaseUpdated, <UpdateEvent>{
|
||||
type: "update",
|
||||
collection: this.type,
|
||||
@@ -286,12 +284,6 @@ export class SQLCollection<
|
||||
.selectFrom<keyof DatabaseSchema>(this.type)
|
||||
.select((a) => a.fn.count<number>("id").as("count"))
|
||||
.where(isFalse("synced"))
|
||||
.$if(this.type === "content", (eb) =>
|
||||
eb.where("conflicted", "is", null)
|
||||
)
|
||||
.$if(this.type === "notes", (eb) =>
|
||||
eb.where("conflicted", "is not", true)
|
||||
)
|
||||
.$if(this.type === "attachments", (eb) =>
|
||||
eb.where((eb) =>
|
||||
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
|
||||
@@ -312,12 +304,6 @@ export class SQLCollection<
|
||||
.selectAll()
|
||||
.$if(lastRowId != null, (qb) => qb.where("id", ">", lastRowId!))
|
||||
.$if(!forceSync, (eb) => eb.where(isFalse("synced")))
|
||||
.$if(this.type === "content", (eb) =>
|
||||
eb.where("conflicted", "is", null)
|
||||
)
|
||||
.$if(this.type === "notes", (eb) =>
|
||||
eb.where("conflicted", "is not", true)
|
||||
)
|
||||
.$if(this.type === "attachments", (eb) =>
|
||||
eb.where((eb) =>
|
||||
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
|
||||
|
||||
@@ -66,7 +66,7 @@ export function parseInternalLink(link: string): InternalLink | undefined {
|
||||
}
|
||||
|
||||
export function isInternalLink(link: string) {
|
||||
return link && link.startsWith("nn://");
|
||||
return link.startsWith("nn://");
|
||||
}
|
||||
|
||||
function isValidInternalType(type: string): type is InternalLinkType {
|
||||
|
||||
22
packages/editor-mobile/package-lock.json
generated
22
packages/editor-mobile/package-lock.json
generated
@@ -4376,7 +4376,7 @@
|
||||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/q": {
|
||||
"version": "1.5.8",
|
||||
@@ -4400,7 +4400,7 @@
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@@ -4435,7 +4435,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==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
@@ -9736,7 +9736,7 @@
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
@@ -17822,6 +17822,20 @@
|
||||
"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",
|
||||
|
||||
@@ -478,8 +478,7 @@ const Tiptap = ({
|
||||
<p
|
||||
style={{
|
||||
width: "90%",
|
||||
fontSize: "0.9rem",
|
||||
color: colors.primary.paragraph
|
||||
fontSize: "0.9rem"
|
||||
}}
|
||||
>
|
||||
<ol>
|
||||
|
||||
@@ -19,13 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import {
|
||||
EditorState,
|
||||
Plugin,
|
||||
PluginKey,
|
||||
Transaction,
|
||||
TextSelection
|
||||
} from "prosemirror-state";
|
||||
import { EditorState, Plugin, PluginKey, Transaction } from "prosemirror-state";
|
||||
import { SearchSettings } from "../../toolbar/stores/search-store";
|
||||
|
||||
type DispatchFn = (tr: Transaction) => void;
|
||||
@@ -280,58 +274,44 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
|
||||
},
|
||||
moveToNextResult:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
({ state, dispatch, commands }) => {
|
||||
const { selectedIndex, results } = this.storage;
|
||||
if (!results || results.length <= 0) return false;
|
||||
|
||||
let nextIndex = selectedIndex + 1;
|
||||
if (isNaN(nextIndex) || nextIndex >= results.length) nextIndex = 0;
|
||||
|
||||
const { tr } = state;
|
||||
const { from, to } = results[nextIndex];
|
||||
tr.setSelection(
|
||||
TextSelection.create(
|
||||
tr.doc,
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(to)
|
||||
)
|
||||
);
|
||||
commands.setTextSelection({ from, to });
|
||||
scrollIntoView();
|
||||
|
||||
this.storage.selectedIndex = nextIndex;
|
||||
tr.setMeta("selectedIndex", nextIndex);
|
||||
state.tr.setMeta("selectedIndex", nextIndex);
|
||||
if (dispatch) updateView(state, dispatch);
|
||||
return true;
|
||||
},
|
||||
moveToPreviousResult:
|
||||
() =>
|
||||
({ state, dispatch }) => {
|
||||
({ state, dispatch, commands }) => {
|
||||
const { selectedIndex, results } = this.storage;
|
||||
if (!results || results.length <= 0) return false;
|
||||
|
||||
let prevIndex = selectedIndex - 1;
|
||||
if (isNaN(prevIndex) || prevIndex < 0) prevIndex = results.length - 1;
|
||||
|
||||
const { tr } = state;
|
||||
const { from, to } = results[prevIndex];
|
||||
tr.setSelection(
|
||||
TextSelection.create(
|
||||
tr.doc,
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(to)
|
||||
)
|
||||
);
|
||||
commands.setTextSelection({ from, to });
|
||||
scrollIntoView();
|
||||
|
||||
this.storage.selectedIndex = prevIndex;
|
||||
tr.setMeta("selectedIndex", prevIndex);
|
||||
state.tr.setMeta("selectedIndex", prevIndex);
|
||||
if (dispatch) updateView(state, dispatch);
|
||||
|
||||
return true;
|
||||
},
|
||||
replace:
|
||||
(term) =>
|
||||
({ chain, tr, dispatch }) => {
|
||||
({ commands, tr, dispatch }) => {
|
||||
const { selectedIndex, results } = this.storage;
|
||||
|
||||
if (!dispatch || !results || results.length <= 0) return false;
|
||||
@@ -340,7 +320,9 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
|
||||
const { from, to } = results[index];
|
||||
|
||||
tr.insertText(term, from, to);
|
||||
return chain().moveToNextResult().run();
|
||||
dispatch(tr);
|
||||
commands.moveToNextResult();
|
||||
return true;
|
||||
},
|
||||
replaceAll:
|
||||
(term) =>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useLayoutEffect } from "react";
|
||||
import { FloatingMenuProps } from "./types";
|
||||
import { SearchReplacePopup } from "../popups/search-replace";
|
||||
import { ResponsivePresenter } from "../../components/responsive";
|
||||
import { getToolbarElement } from "../utils/dom";
|
||||
import { getEditorContainer, getToolbarElement } from "../utils/dom";
|
||||
import { useEditorSearchStore } from "../stores/search-store";
|
||||
|
||||
export function SearchReplaceFloatingMenu(props: FloatingMenuProps) {
|
||||
@@ -41,11 +41,11 @@ export function SearchReplaceFloatingMenu(props: FloatingMenuProps) {
|
||||
isOpen={isSearching}
|
||||
onClose={() => editor.commands.endSearch()}
|
||||
position={{
|
||||
target: getToolbarElement(),
|
||||
target: editor.isEditable ? getToolbarElement() : getEditorContainer(),
|
||||
isTargetAbsolute: true,
|
||||
location: "below",
|
||||
location: editor.isEditable ? "below" : "top",
|
||||
align: "end",
|
||||
yOffset: 5
|
||||
yOffset: editor.isEditable ? 5 : -50
|
||||
}}
|
||||
blocking={false}
|
||||
focusOnRender={false}
|
||||
|
||||
@@ -61,9 +61,7 @@ export function Toolbar(props: ToolbarProps) {
|
||||
? editor.isEditable
|
||||
? [...MOBILE_STATIC_TOOLBAR_GROUPS, ...tools]
|
||||
: READONLY_MOBILE_STATIC_TOOLBAR_GROUPS
|
||||
: editor.isEditable
|
||||
? [...STATIC_TOOLBAR_GROUPS, ...tools]
|
||||
: [],
|
||||
: [...STATIC_TOOLBAR_GROUPS, ...tools],
|
||||
[tools, editor.isEditable, isMobile]
|
||||
);
|
||||
|
||||
|
||||
@@ -301,5 +301,5 @@ function LinkTool(props: LinkToolProps) {
|
||||
}
|
||||
|
||||
function isInternalLink(href?: string | null) {
|
||||
return typeof href === "string" ? href.startsWith("nn://") : false;
|
||||
return !!href?.startsWith("nn://");
|
||||
}
|
||||
|
||||
@@ -282,10 +282,9 @@ img.ProseMirror-separator {
|
||||
background-color: var(--paragraph, var(--nn_primary_paragraph)) !important;
|
||||
}
|
||||
|
||||
.search-result.selected,
|
||||
.search-result.selected::selection {
|
||||
.search-result.selected {
|
||||
background-color: var(--accent-secondary) !important;
|
||||
color: var(--accentForeground-secondary) !important;
|
||||
color: var(--accentForeground-secondary);
|
||||
}
|
||||
|
||||
.search-result {
|
||||
|
||||
Reference in New Issue
Block a user