Compare commits

..

1 Commits

Author SHA1 Message Date
Ammar Ahmed
dd94d9685c mobile: fix cannot read property 'groupBy' of undefined 2024-05-11 11:09:30 +05:00
74 changed files with 354 additions and 678 deletions

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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
);

View File

@@ -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,

View File

@@ -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 {

View File

@@ -174,7 +174,6 @@ export const Dialog = ({ context = "global" }) => {
onSubmit={onPressPositive}
returnKeyLabel="Done"
returnKeyType="done"
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
</View>

View File

@@ -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}

View File

@@ -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);

View File

@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { 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
});
}
}}
/>

View File

@@ -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",

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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");
},

View File

@@ -59,14 +59,14 @@ const recoveryKeyMessage = {
() => {
eSendEvent(eOpenRecoveryKeyDialog);
},
false,
true,
async () => {
SettingsService.set({
recoveryKeySaved: true
});
clearMessage();
},
"Cancel"
"I have saved my key already"
);
},
data: {},

View File

@@ -952,13 +952,13 @@ async function pinNote(id: string) {
const note = await db.notes.note(id as string);
if (!note) return;
let text = await convertNoteToText(note, 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"],

View File

@@ -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 {

View File

@@ -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) => ({

View File

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

View File

@@ -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'

View File

@@ -1,3 +1,4 @@
- Added push/pull changes to troubleshoot sync issues in settings
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -1015,7 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 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)";

View File

@@ -959,4 +959,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
COCOAPODS: 1.15.2
COCOAPODS: 1.12.1

View File

@@ -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": [

View File

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

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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);
});

View File

@@ -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": {

View File

@@ -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/",

View File

@@ -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;

View File

@@ -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
);
});

View File

@@ -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(

View File

@@ -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);
});

View File

@@ -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());
}
});

View File

@@ -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({

View File

@@ -307,7 +307,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
break;
}
case "backup": {
await createBackup();
await createBackup(true);
break;
}
default: {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}}

View File

@@ -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();
}
};
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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");
}}
>

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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(

View File

@@ -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;

View File

@@ -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", () =>

View File

@@ -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);

View File

@@ -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 =

View File

@@ -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

View File

@@ -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:");
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 }
);
}
}
}

View File

@@ -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)
)

View File

@@ -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))

View File

@@ -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>;

View File

@@ -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
) {

View File

@@ -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)])

View File

@@ -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 {

View File

@@ -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",

View File

@@ -478,8 +478,7 @@ const Tiptap = ({
<p
style={{
width: "90%",
fontSize: "0.9rem",
color: colors.primary.paragraph
fontSize: "0.9rem"
}}
>
<ol>

View File

@@ -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) =>

View File

@@ -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}

View File

@@ -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]
);

View File

@@ -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://");
}

View File

@@ -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 {