mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 02:29:18 +02:00
Compare commits
1 Commits
3.0.8-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.6",
|
||||
"version": "3.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.6",
|
||||
"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.6",
|
||||
"version": "3.0.3",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -16,16 +16,15 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "react-native-gesture-handler";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { initializeLogger } from "./common/database/logger";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
@@ -45,28 +44,23 @@ I18nManager.swapLeftAndRightInRTL(false);
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
@@ -44,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";
|
||||
@@ -117,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);
|
||||
@@ -133,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"}`
|
||||
@@ -210,7 +214,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -223,7 +227,6 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
@@ -236,7 +239,7 @@ export async function deriveCryptoKey(data) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
@@ -261,12 +264,13 @@ export async function getCryptoKey(_name) {
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
key: await getDatabaseKey()
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info("User key decrypted: ", !!key);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
|
||||
@@ -85,10 +85,4 @@ export async function setupDatabase(password) {
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
let DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
|
||||
const setLogger = () => {
|
||||
DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
};
|
||||
|
||||
export { DatabaseLogger, setLogger };
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -16,27 +16,14 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const initializeLogger = async () => {
|
||||
await initialize({
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
return new RNSqliteDriver({ async: true, dbName: name });
|
||||
},
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
|
||||
});
|
||||
setLogger();
|
||||
};
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
export { initializeLogger };
|
||||
initialize(new KV(LoggerStorage));
|
||||
|
||||
export { LoggerStorage };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -112,10 +112,6 @@ export const SessionExpired = () => {
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
|
||||
const key = await db.user.getEncryptionKey();
|
||||
if (!key) throw new Error("No encryption key found.");
|
||||
|
||||
Sync.run("global", false, "full", async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
@@ -131,6 +127,7 @@ export const SessionExpired = () => {
|
||||
setVisible(false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -185,8 +185,7 @@ const camera = async (options: PickerOptions) => {
|
||||
cropping: false,
|
||||
multiple: true,
|
||||
maxFiles: 10,
|
||||
writeTempFile: true,
|
||||
compressImageQuality: 1
|
||||
writeTempFile: true
|
||||
})
|
||||
.then((response) => {
|
||||
handleImageResponse(
|
||||
@@ -216,8 +215,7 @@ const gallery = async (options: PickerOptions) => {
|
||||
mediaType: "photo",
|
||||
maxFiles: 10,
|
||||
cropping: false,
|
||||
multiple: true,
|
||||
compressImageQuality: 1
|
||||
multiple: true
|
||||
})
|
||||
.then((response) =>
|
||||
handleImageResponse(
|
||||
@@ -268,6 +266,7 @@ const handleImageResponse = async (
|
||||
response: Image[],
|
||||
options: PickerOptions
|
||||
) => {
|
||||
console.log(response, "result-file-picker");
|
||||
const result = await AttachImage.present(response, options.context);
|
||||
if (!result) return;
|
||||
const compress = result.compress;
|
||||
@@ -303,15 +302,10 @@ const handleImageResponse = async (
|
||||
type: "url"
|
||||
});
|
||||
|
||||
let fileName = image.sourceURL
|
||||
const fileName = image.sourceURL
|
||||
? basename(image.sourceURL)
|
||||
: image.filename || "image";
|
||||
|
||||
fileName =
|
||||
image.mime === "image/jpeg"
|
||||
? fileName.replace(/HEIC|HEIF/, "jpeg")
|
||||
: fileName;
|
||||
|
||||
console.log("attaching image...", fileName);
|
||||
|
||||
console.log("attaching 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;
|
||||
|
||||
@@ -122,8 +122,7 @@ export default function DebugLogs() {
|
||||
colors.primary.paragraph,
|
||||
colors.error.paragraph,
|
||||
colors.static.black,
|
||||
colors.static.orange,
|
||||
colors.primary.border
|
||||
colors.static.orange
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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 3016
|
||||
versionCode 3011
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -37,10 +37,4 @@ allprojects {
|
||||
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = 2105;
|
||||
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.8;
|
||||
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 = 2105;
|
||||
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.8;
|
||||
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 = 2105;
|
||||
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.8;
|
||||
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 = 2105;
|
||||
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.8;
|
||||
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 = 2105;
|
||||
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.8;
|
||||
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 = 2105;
|
||||
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.8;
|
||||
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.8",
|
||||
"version": "3.0.3",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -10,19 +10,3 @@ index 5de0845..1b158d8 100644
|
||||
|
||||
if (includeBase64) {
|
||||
image.putString("data", getBase64StringFromFile(compressedImagePath));
|
||||
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
index 9f20973..5e14da8 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
@@ -595,8 +595,10 @@ - (void)qb_imagePickerController:
|
||||
NSString *mimeType = [self determineMimeTypeFromImageData:imageData];
|
||||
Boolean isKnownMimeType = [mimeType length] > 0;
|
||||
|
||||
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
|
||||
+
|
||||
ImageResult *imageResult = [[ImageResult alloc] init];
|
||||
- if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg) {
|
||||
+ if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg && !isHeicOrHeif) {
|
||||
// Use original, unmodified image
|
||||
imageResult.data = imageData;
|
||||
imageResult.width = @(imgT.size.width);
|
||||
|
||||
@@ -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"
|
||||
|
||||
33
apps/theme-builder/package-lock.json
generated
33
apps/theme-builder/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -18,18 +18,17 @@
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
"@trpc/client": "^10.38.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"clipboard-polyfill": "^4.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"katex": "^0.16.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react": "^18.2.17",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -979,7 +978,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -2027,15 +2026,6 @@
|
||||
"@trpc/server": "10.38.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "10.38.3",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.38.3.tgz",
|
||||
"integrity": "sha512-9s8/kwo2IDB5hwB2SKZZrfevRhdb1f9fdXtIYd3lbQuf2jQaC/LyQuHaIQjDQoUx9updBfsHXcFFPiCP1DL6pg==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.1",
|
||||
"dev": true,
|
||||
@@ -2108,30 +2098,25 @@
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.5",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
|
||||
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"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=="
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/styled-system": {
|
||||
"version": "5.1.16",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/theme-builder",
|
||||
"description": "Your private note taking space",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -17,18 +17,18 @@
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
"@trpc/client": "^10.38.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"clipboard-polyfill": "^4.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"katex": "^0.16.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react": "^18.2.17",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAHUUExURUxpcf////////////////////////////////////////////////////////////////////////////////////39/f39/f////////////////////////////////z8/O3s7P7+/vTz8wAAAPz7++/v7/Py8u/u7vDv7+7t7dfX1/79/ezr66Kiovv6+vf29vX09OTj4w0NDfv7++Ti4vr6+vT09Ovq6vn5+f39/fb19dnX1/b29tvb29/f3xMTE/j4+Pn4+O7s7MnJyerq6uno6M/Pz8jIyN3d3dnZ2d7e3kZGRvj39/38/Pf39+zs7PDw8OPi4ubl5cvLy+rp6czMzPLy8vPz887Ozujo6MrKyuDf3+fn59PT0+bm5uXl5c3NzfX19evr6wsLC+/t7ePj4/Ty8u3r6/Lx8eXk5OPh4dDQ0Nza2tjX19vZ2fHw8Nva2tLR0djW1tbW1tra2tLS0uHh4evp6eLi4tXV1djY2L+/vysrK7q6utHR0aGhoa6urrm5uQICAigoKCEhIZCQkFxcXBgYGFRUVBISEm1tbQ4ODklJSbu7uyAgID09PZqamkVFRXh4eLGxsTg4OOTk5FFRUQEBAQoKCnx8fBcXF7/+rP8AAAAfdFJOUwBOqO2h6KT9Mtyr5+w3Str+6TTZOjXb31M24O8BOd0PapSsAAABxElEQVQ4y2NgYOBgYWWXxwLYBdmEGICAjx9Z1NJJVQfB42QE6hdG0RaoaaCqhKSCl4EF1VxNVAXyTAzcaArUUBXwMLBjKDBBViDAII+uwBZFgTy6AjU1awIKNKz1aa0g1VBfB58CDS08CuIb40AKzLAqsJKXN2tXacChwKS+rTtB3tJHRU9ewxmbFZ6tKiqTrSAKdIO1LDEUaPeoqMzsUwAp0NIt8Uj0ijJXQFVgnDJVZcK0XrACXcXY0sjoHM8AfyUkBbOn9KvMmgRSYFEW4hUVqJagHeaShqTAwX7ijJQusAkW5VXRHu6eruFqaigKkqarqAAVKFbYxWiYq1XGedijuMHBWz6xo7m2Rl7RwtvdJaw6NjzCFl2BVVNdshJQgbqBsqtdpIu7nasjXIFXS2cyRL2CW6i6JTCgFCLitUPs4QrM7cDmKZi7FRYVm6o5QeLbDGICLNEqJAVk59rk2djY5Me4FThawQOKFSzrrxmaZWpqBAamQUGmwRaakNTNzMAm7+ekrKmlqOjsrIgAGcAA1ci0NVPiYhAV8zVURgbp6nBg7isuwsDAKKfkp4AEVOHASklKApS9JZmY5bECaS5ZGQYA3vqpy6NoYh0AAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="
|
||||
/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#01c352" />
|
||||
<meta name="msapplication-TileColor" content="#01c352" />
|
||||
<meta name="theme-color" content="#01c352" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
@@ -40,26 +49,13 @@
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<title>Notesnook Theme Builder</title>
|
||||
<style id="theme-colors">
|
||||
#splash {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
import { themeToCSS } from "@notesnook/theme";
|
||||
|
||||
<script nonce="7WIq8hRwApoXhctoGZZthMLYQLRNiprTwcPi6Azdf">
|
||||
const colorScheme = JSON.parse(
|
||||
window.localStorage.getItem("colorScheme") || '"light"'
|
||||
);
|
||||
const root = document.querySelector("html");
|
||||
if (root) root.setAttribute("data-theme", colorScheme);
|
||||
|
||||
const theme = window.localStorage.getItem(`theme:${colorScheme}`);
|
||||
if (theme) {
|
||||
const css = themeToCSS(JSON.parse(theme));
|
||||
const stylesheet = document.getElementById("theme-colors");
|
||||
if (stylesheet) stylesheet.innerHTML = css;
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
<style>
|
||||
@@ -67,6 +63,21 @@
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--bg: #0f0f0f;
|
||||
--fg: #fff;
|
||||
--three-bars-bg: #494949;
|
||||
--gradient-stop-color: #111111;
|
||||
--n-letter-color: #e1e1e1;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--bg: #fff;
|
||||
--three-bars-bg: #bebebe;
|
||||
--gradient-stop-color: #fff9f9;
|
||||
--n-letter-color: #000;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .react-loading-skeleton {
|
||||
--base-color: var(--background-secondary);
|
||||
--highlight-color: #var(--background-secondary);
|
||||
@@ -77,19 +88,6 @@
|
||||
--highlight-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
#splash {
|
||||
background-color: var(--background);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#splash svg {
|
||||
transform: scale(1);
|
||||
animation: pulse 2s infinite;
|
||||
@@ -97,7 +95,7 @@
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
70% {
|
||||
@@ -105,7 +103,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +128,20 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@keyframes fadeUp {
|
||||
0% {
|
||||
transform: translateY(500px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateY(0px);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* svg {
|
||||
@@ -162,40 +171,85 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="theme-scope-base">
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/highlightjs@9.16.2/highlight.pack.min.js"></script>
|
||||
-->
|
||||
<div id="root"></div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 0; width: 0; z-index: -1; position: absolute"
|
||||
<div
|
||||
id="splash"
|
||||
style="
|
||||
background-color: var(--bg);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<symbol id="full-logo" viewBox="0 0 1000 1000">
|
||||
<rect
|
||||
width="1000"
|
||||
height="1000"
|
||||
rx="30%"
|
||||
ry="30%"
|
||||
style="fill: var(--paragraph)"
|
||||
/>
|
||||
<g transform="translate(275,176.95254)">
|
||||
<svg viewBox="0 0 339 339" style="width: 150px">
|
||||
<defs />
|
||||
<defs>
|
||||
<linearGradient
|
||||
xlink:href="#a"
|
||||
id="b"
|
||||
x1="188.61227"
|
||||
x2="193.54405"
|
||||
y1="165.2058"
|
||||
y2="216.81519"
|
||||
gradientTransform="rotate(5 4448 -4204) scale(2.93671)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
/>
|
||||
|
||||
<linearGradient id="a">
|
||||
<stop offset="0" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="var(--gradient-stop-color)"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="c"
|
||||
x1="167.8"
|
||||
x2="270.6"
|
||||
y1="76.9"
|
||||
y2="64.2"
|
||||
gradientTransform="rotate(5 465 -2050) scale(1.50082)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
xlink:href="#a"
|
||||
/>
|
||||
</defs>
|
||||
<g transform="translate(0 42)">
|
||||
<path fill="url(#b)" d="M160 205l154 42-141 44-155-42z" />
|
||||
<path fill="url(#c)" d="M160-35v240l154 42 1-253z" />
|
||||
<path
|
||||
style="fill: var(--background)"
|
||||
d="M437.985 493.919c-17.255 50.411-51.835 93.065-97.588 120.372-45.752 27.307-99.71 37.496-152.269 28.753-52.56-8.743-100.313999-35.85-134.763-76.498C18.916 525.898.006 474.347 0 421.064v-110.25l79.121001 33.053v77.152c-.007 20.779 4.434 41.318 13.024 60.237C100.735 500.176 113.276 517.037 128.925 530.707c2.97 2.587 6.053 5.107 9.236 7.459 23.357 17.388 51.402 27.352 80.494 28.597.99 0 1.946.079 2.925.101.979.023 2.25 0 3.375 0h3.375c1.125 0 1.935 0 2.925-.101 29.081-1.248 57.116-11.203 80.471-28.575 3.173-2.351 6.255-4.86 9.237-7.447 20.556-17.98 35.656-41.366 43.582-67.5zM450 225v196.065c0 2.531 0 5.074-.158 7.605l-78.963-33.019V225c-.013-37.684-14.607-73.901-40.725-101.066-26.119-27.164003-61.735-43.168003-99.389-44.660003-37.655-1.492-74.426 11.644-102.611 36.657003-28.185999 25.013-45.599999 59.962-48.593999 97.526-.281 3.803-.439 7.662-.439 11.543v48.712L0 240.637V0h225c59.674 0 116.903 23.705 159.099 65.901C426.295 108.097 450 165.326 450 225Z"
|
||||
fill="none"
|
||||
stroke-width="1.2"
|
||||
d="M160 205V-35m0 240L18 249m142-44l154 41"
|
||||
/>
|
||||
<path
|
||||
fill="var(--n-letter-color)"
|
||||
d="M84 109l35 54V98l21-7v91l-27 9-35-54v65l-21 6v-91z"
|
||||
/>
|
||||
<rect
|
||||
width="86.1"
|
||||
height="12.6"
|
||||
x="185"
|
||||
y="97"
|
||||
fill="var(--three-bars-bg)"
|
||||
ry="2.3"
|
||||
transform="skewY(15) scale(.9669 1)"
|
||||
/>
|
||||
<path
|
||||
fill="var(--three-bars-bg)"
|
||||
d="M181 169l99 26 2 3v8c0 1-1 2-2 1l-99-26-2-3v-7c0-2 1-2 2-2zm0-47l99 27 2 2v8l-2 2-99-27c-1 0-2-1-2-3v-7l2-2z"
|
||||
/>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="themed-logo" viewBox="0 0 450 646.09491">
|
||||
<path
|
||||
style="fill: var(--paragraph)"
|
||||
d="M437.985 493.919c-17.255 50.411-51.835 93.065-97.588 120.372-45.752 27.307-99.71 37.496-152.269 28.753-52.56-8.743-100.313999-35.85-134.763-76.498C18.916 525.898.006 474.347 0 421.064v-110.25l79.121001 33.053v77.152c-.007 20.779 4.434 41.318 13.024 60.237C100.735 500.176 113.276 517.037 128.925 530.707c2.97 2.587 6.053 5.107 9.236 7.459 23.357 17.388 51.402 27.352 80.494 28.597.99 0 1.946.079 2.925.101.979.023 2.25 0 3.375 0h3.375c1.125 0 1.935 0 2.925-.101 29.081-1.248 57.116-11.203 80.471-28.575 3.173-2.351 6.255-4.86 9.237-7.447 20.556-17.98 35.656-41.366 43.582-67.5zM450 225v196.065c0 2.531 0 5.074-.158 7.605l-78.963-33.019V225c-.013-37.684-14.607-73.901-40.725-101.066-26.119-27.164003-61.735-43.168003-99.389-44.660003-37.655-1.492-74.426 11.644-102.611 36.657003-28.185999 25.013-45.599999 59.962-48.593999 97.526-.281 3.803-.439 7.662-.439 11.543v48.712L0 240.637V0h225c59.674 0 116.903 23.705 159.099 65.901C426.295 108.097 450 165.326 450 225Z"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<div id="splash" class="hidden">
|
||||
<svg style="height: 120px">
|
||||
<use href="#themed-logo" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="dialogContainer"></div>
|
||||
|
||||
@@ -18,59 +18,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { init } from "@notesnook/web/src/bootstrap";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
ErrorBoundary,
|
||||
ErrorComponent
|
||||
} from "@notesnook/web/src/components/error-boundary";
|
||||
import { render } from "react-dom";
|
||||
import { BaseThemeProvider } from "@notesnook/web/src/components/theme-provider";
|
||||
import { App } from "./app";
|
||||
|
||||
renderApp();
|
||||
|
||||
async function renderApp() {
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) return;
|
||||
const root = createRoot(rootElement);
|
||||
try {
|
||||
const { component, props } = await init();
|
||||
const { component, props } = await init();
|
||||
|
||||
const { useKeyStore } = await import(
|
||||
"@notesnook/web/src/interfaces/key-store"
|
||||
);
|
||||
await useKeyStore.getState().init();
|
||||
|
||||
const { default: Component } = await component();
|
||||
const { default: AppLock } = await import(
|
||||
"@notesnook/web/src/views/app-lock"
|
||||
);
|
||||
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
sx={{
|
||||
display: "flex",
|
||||
"#app": { flex: 1, height: "unset" },
|
||||
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<AppLock>
|
||||
<Component route={props?.route || "login:email"} />
|
||||
</AppLock>
|
||||
<App />
|
||||
</BaseThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} catch (e) {
|
||||
root.render(
|
||||
<>
|
||||
<ErrorComponent
|
||||
error={e}
|
||||
resetErrorBoundary={() => window.location.reload()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const { default: Component } = await component();
|
||||
render(
|
||||
<BaseThemeProvider
|
||||
sx={{
|
||||
display: "flex",
|
||||
"#app": { flex: 1, height: "unset" },
|
||||
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Component route={props?.route || "login:email"} />
|
||||
<App />
|
||||
</BaseThemeProvider>,
|
||||
document.getElementById("root"),
|
||||
() => {
|
||||
document.getElementById("splash")?.remove();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.6",
|
||||
"version": "3.0.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.6",
|
||||
"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.6",
|
||||
"version": "3.0.3",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -58,7 +58,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
database.setup({
|
||||
sqliteOptions: {
|
||||
dialect: (name, init) => createDialect(name, true, init),
|
||||
dialect: createDialect,
|
||||
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -40,7 +40,7 @@ class SqliteDriver extends KSqliteDriver {
|
||||
}
|
||||
}
|
||||
|
||||
export const createDialect = (name: string, _encrypted: boolean): Dialect => {
|
||||
export const createDialect = (name: string): Dialect => {
|
||||
return {
|
||||
createDriver: () =>
|
||||
new SqliteDriver({
|
||||
|
||||
@@ -34,7 +34,6 @@ declare module "kysely" {
|
||||
|
||||
export const createDialect = (
|
||||
name: string,
|
||||
encrypted: boolean,
|
||||
init?: () => Promise<void>
|
||||
): Dialect => {
|
||||
return {
|
||||
@@ -42,7 +41,6 @@ export const createDialect = (
|
||||
new WaSqliteWorkerDriver({
|
||||
async: !isFeatureSupported("opfs"),
|
||||
dbName: name,
|
||||
encrypted,
|
||||
init
|
||||
}),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -32,230 +32,219 @@ type PreparedStatement = {
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
class _SQLiteWorker {
|
||||
sqlite!: SQLiteAPI;
|
||||
db: number | undefined = undefined;
|
||||
vfs: IDBBatchAtomicVFS | AccessHandlePoolVFS | null = null;
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, PreparedStatement> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor(
|
||||
private readonly dbName: string,
|
||||
private readonly encrypted: boolean
|
||||
) {
|
||||
console.log("new sqlite worker", dbName, encrypted);
|
||||
let sqlite: SQLiteAPI;
|
||||
let db: number | undefined = undefined;
|
||||
let vfs: IDBBatchAtomicVFS | AccessHandlePoolVFS | null = null;
|
||||
let initialized = false;
|
||||
const preparedStatements: Map<string, PreparedStatement> = new Map();
|
||||
const retryCounter: Record<string, number> = {};
|
||||
console.log("new sqlite worker");
|
||||
|
||||
async function open(dbName: string, async: boolean, url?: string) {
|
||||
if (db) {
|
||||
console.error("Database is already initialized", db);
|
||||
return;
|
||||
}
|
||||
|
||||
async open(async: boolean, url?: string) {
|
||||
if (this.db) {
|
||||
console.error("Database is already initialized", this.db);
|
||||
return;
|
||||
}
|
||||
|
||||
const option = url ? { locateFile: () => url } : {};
|
||||
const sqliteModule = async
|
||||
? await import("./wa-sqlite-async").then(
|
||||
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
||||
)
|
||||
: await import("./wa-sqlite").then(({ default: SQLiteSyncESMFactory }) =>
|
||||
SQLiteSyncESMFactory(option)
|
||||
);
|
||||
this.sqlite = Factory(sqliteModule);
|
||||
this.vfs = await this.getVFS(this.dbName, async);
|
||||
|
||||
this.sqlite.vfs_register(this.vfs, false);
|
||||
this.db = await this.sqlite.open_v2(
|
||||
this.dbName,
|
||||
undefined,
|
||||
`multipleciphers-${this.vfs.name}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async prepare(sql: string): Promise<PreparedStatement | undefined> {
|
||||
if (!this.db) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = this.preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const str = this.sqlite.str_new(this.db, sql);
|
||||
const prepared = await this.sqlite.prepare_v2(
|
||||
this.db,
|
||||
this.sqlite.str_value(str)
|
||||
const option = url ? { locateFile: () => url } : {};
|
||||
const sqliteModule = async
|
||||
? await import("./wa-sqlite-async").then(
|
||||
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
||||
)
|
||||
: await import("./wa-sqlite").then(({ default: SQLiteSyncESMFactory }) =>
|
||||
SQLiteSyncESMFactory(option)
|
||||
);
|
||||
if (!prepared) return;
|
||||
sqlite = Factory(sqliteModule);
|
||||
vfs = await getVFS(dbName, async);
|
||||
|
||||
const statement: PreparedStatement = {
|
||||
stmt: prepared.stmt,
|
||||
columns: this.sqlite.column_names(prepared.stmt)
|
||||
};
|
||||
this.preparedStatements.set(sql, statement);
|
||||
sqlite.vfs_register(vfs, false);
|
||||
db = await sqlite.open_v2(dbName, undefined, `multipleciphers-${vfs.name}`);
|
||||
}
|
||||
|
||||
this.sqlite.str_finish(str);
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async function prepare(sql: string) {
|
||||
if (!db) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return statement;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
const str = sqlite.str_new(db, sql);
|
||||
const prepared = await sqlite.prepare_v2(db, sqlite.str_value(str));
|
||||
if (!prepared) return;
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (this.retryCounter[sql] < 5) {
|
||||
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return this.prepare(sql);
|
||||
} else this.retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error || ex instanceof SQLiteError)
|
||||
ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async exec(sql: string, mode: RunMode, parameters?: SQLiteCompatibleType[]) {
|
||||
const prepared = await this.prepare(sql);
|
||||
if (!prepared) return [];
|
||||
try {
|
||||
if (parameters) this.sqlite.bind_collection(prepared.stmt, parameters);
|
||||
|
||||
// fast path for exec statements
|
||||
if (mode === "exec") {
|
||||
while ((await this.sqlite.step(prepared.stmt)) === SQLITE_ROW);
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: Record<string, SQLiteCompatibleType>[] = [];
|
||||
while ((await this.sqlite.step(prepared.stmt)) === SQLITE_ROW) {
|
||||
const row = this.sqlite.row(prepared.stmt);
|
||||
const acc: Record<string, SQLiteCompatibleType> = {};
|
||||
row.forEach((v, i) => (acc[prepared.columns[i]] = v));
|
||||
rows.push(acc);
|
||||
}
|
||||
|
||||
return rows;
|
||||
} catch (e) {
|
||||
if (e instanceof Error || e instanceof SQLiteError)
|
||||
e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
} finally {
|
||||
await this.sqlite
|
||||
.reset(prepared.stmt)
|
||||
// we must clear/destruct the prepared statement if it can't be reset
|
||||
.catch(() =>
|
||||
this.sqlite
|
||||
.finalize(prepared.stmt)
|
||||
// ignore error (we will just prepare a new statement)
|
||||
.catch(console.error)
|
||||
.finally(() => this.preparedStatements.delete(sql))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
mode: RunMode,
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
|
||||
await this.waitForDatabase();
|
||||
}
|
||||
if (!this.db) throw new Error("No database is not opened.");
|
||||
|
||||
const rows = (await this.exec(sql, mode, parameters)) as R[];
|
||||
if (mode === "query") return { rows };
|
||||
|
||||
// initialize the database after it has been successfully decrypted.
|
||||
// all queries prior to that must wait otherwise we get the
|
||||
// "file is not a database" error
|
||||
if (this.encrypted && sql.startsWith("PRAGMA key")) await this.initialize();
|
||||
|
||||
return {
|
||||
insertId: BigInt(this.sqlite.last_insert_rowid(this.db)),
|
||||
numAffectedRows: BigInt(this.sqlite.changes(this.db)),
|
||||
rows: mode === "raw" ? rows : []
|
||||
const statement: PreparedStatement = {
|
||||
stmt: prepared.stmt,
|
||||
columns: sqlite.column_names(prepared.stmt)
|
||||
};
|
||||
}
|
||||
preparedStatements.set(sql, statement);
|
||||
|
||||
async close() {
|
||||
if (!this.db) return;
|
||||
sqlite.str_finish(str);
|
||||
|
||||
for (const [_, prepared] of this.preparedStatements) {
|
||||
await this.sqlite.finalize(prepared.stmt);
|
||||
}
|
||||
this.preparedStatements.clear();
|
||||
await this.sqlite.close(this.db);
|
||||
await this.vfs?.close();
|
||||
// reset retry count on success
|
||||
retryCounter[sql] = 0;
|
||||
return statement;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
this.db = undefined;
|
||||
this.initialized = false;
|
||||
}
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (retryCounter[sql] < 5) {
|
||||
retryCounter[sql] = (retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return prepare(sql);
|
||||
} else retryCounter[sql] = 0;
|
||||
|
||||
async export(dbName: string, async: boolean) {
|
||||
const vfs = await this.getVFS(dbName, async);
|
||||
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
|
||||
return transfer(stream, [stream]);
|
||||
}
|
||||
|
||||
async delete(dbName: string, async: boolean) {
|
||||
await this.close();
|
||||
if (this.vfs) await this.vfs.delete();
|
||||
else await (await this.getVFS(dbName, async)).delete();
|
||||
}
|
||||
|
||||
async getVFS(dbName: string, async: boolean) {
|
||||
const vfs = async
|
||||
? await import("./IDBBatchAtomicVFS").then(
|
||||
({ IDBBatchAtomicVFS }) =>
|
||||
new IDBBatchAtomicVFS(dbName, { durability: "strict" })
|
||||
)
|
||||
: await import("./AccessHandlePoolVFS").then(
|
||||
({ AccessHandlePoolVFS }) => new AccessHandlePoolVFS(dbName)
|
||||
);
|
||||
if ("isReady" in vfs) await vfs.isReady;
|
||||
return vfs;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: { type: "databaseInitialized", dbName: this.dbName }
|
||||
})
|
||||
);
|
||||
console.log("Database initialized", this.db);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async waitForDatabase() {
|
||||
// if the database hasn't yet been initialized.
|
||||
if (!this.initialized) {
|
||||
console.log("Waiting for database to be initialized...", this.db);
|
||||
return await new Promise<boolean>((resolve) =>
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (
|
||||
ev.data.type === "databaseInitialized" &&
|
||||
ev.data.dbName === this.dbName
|
||||
)
|
||||
resolve(true);
|
||||
})
|
||||
);
|
||||
}
|
||||
return true;
|
||||
if (ex instanceof Error || ex instanceof SQLiteError)
|
||||
ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
export type SQLiteWorker = typeof _SQLiteWorker.prototype;
|
||||
async function run(
|
||||
sql: string,
|
||||
mode: RunMode,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
) {
|
||||
const prepared = await prepare(sql);
|
||||
if (!prepared) return [];
|
||||
try {
|
||||
if (parameters) sqlite.bind_collection(prepared.stmt, parameters);
|
||||
|
||||
// fast path for exec statements
|
||||
if (mode === "exec") {
|
||||
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW);
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: Record<string, SQLiteCompatibleType>[] = [];
|
||||
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW) {
|
||||
const row = sqlite.row(prepared.stmt);
|
||||
const acc: Record<string, SQLiteCompatibleType> = {};
|
||||
row.forEach((v, i) => (acc[prepared.columns[i]] = v));
|
||||
rows.push(acc);
|
||||
}
|
||||
|
||||
return rows;
|
||||
} catch (e) {
|
||||
if (e instanceof Error || e instanceof SQLiteError)
|
||||
e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
} finally {
|
||||
await sqlite
|
||||
.reset(prepared.stmt)
|
||||
// we must clear/destruct the prepared statement if it can't be reset
|
||||
.catch(() =>
|
||||
sqlite
|
||||
.finalize(prepared.stmt)
|
||||
// ignore error (we will just prepare a new statement)
|
||||
.catch(console.error)
|
||||
.finally(() => preparedStatements.delete(sql))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function exec<R>(
|
||||
mode: RunMode,
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!sql.startsWith("PRAGMA key")) {
|
||||
await waitForDatabase();
|
||||
}
|
||||
if (!db) throw new Error("No database is not opened.");
|
||||
|
||||
const rows = (await run(sql, mode, parameters)) as R[];
|
||||
if (mode === "query") return { rows };
|
||||
|
||||
// initialize the database after it has been successfully decrypted.
|
||||
// all queries prior to that must wait otherwise we get the
|
||||
// "file is not a database" error
|
||||
if (sql.startsWith("PRAGMA key")) await initialize();
|
||||
|
||||
return {
|
||||
insertId: BigInt(sqlite.last_insert_rowid(db)),
|
||||
numAffectedRows: BigInt(sqlite.changes(db)),
|
||||
rows: mode === "raw" ? rows : []
|
||||
};
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (!db) return;
|
||||
|
||||
for (const [_, prepared] of preparedStatements) {
|
||||
await sqlite.finalize(prepared.stmt);
|
||||
}
|
||||
preparedStatements.clear();
|
||||
await sqlite.close(db);
|
||||
await vfs?.close();
|
||||
|
||||
db = undefined;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
async function exportDatabase(dbName: string, async: boolean) {
|
||||
const vfs = await getVFS(dbName, async);
|
||||
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
|
||||
return transfer(stream, [stream]);
|
||||
}
|
||||
|
||||
async function deleteDatabase(dbName: string, async: boolean) {
|
||||
await close();
|
||||
if (vfs) await vfs.delete();
|
||||
else await (await getVFS(dbName, async)).delete();
|
||||
}
|
||||
|
||||
async function getVFS(dbName: string, async: boolean) {
|
||||
const vfs = async
|
||||
? await import("./IDBBatchAtomicVFS").then(
|
||||
({ IDBBatchAtomicVFS }) =>
|
||||
new IDBBatchAtomicVFS(dbName, { durability: "strict" })
|
||||
)
|
||||
: await import("./AccessHandlePoolVFS").then(
|
||||
({ AccessHandlePoolVFS }) => new AccessHandlePoolVFS(dbName)
|
||||
);
|
||||
if ("isReady" in vfs) await vfs.isReady;
|
||||
return vfs;
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", { data: { type: "databaseInitialized" } })
|
||||
);
|
||||
console.log("Database initialized", db);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
const worker = {
|
||||
close,
|
||||
open,
|
||||
run: exec,
|
||||
export: exportDatabase,
|
||||
delete: deleteDatabase
|
||||
};
|
||||
|
||||
export type SQLiteWorker = typeof worker;
|
||||
|
||||
addEventListener("message", async (event) => {
|
||||
if (!event.data.type) {
|
||||
const worker = new _SQLiteWorker(event.data.dbName, event.data.encrypted);
|
||||
await worker.open(event.data.async, event.data.uri);
|
||||
await worker.open(event.data.dbName, event.data.async, event.data.uri);
|
||||
const providerPort = createSharedServicePort(worker);
|
||||
postMessage(null, [providerPort]);
|
||||
|
||||
self.addEventListener("beforeunload", () => worker.close());
|
||||
}
|
||||
});
|
||||
|
||||
async function waitForDatabase() {
|
||||
// if the database hasn't yet been initialized.
|
||||
if (!initialized) {
|
||||
console.log("Waiting for database to be initialized...", db);
|
||||
return await new Promise<boolean>((resolve) =>
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (ev.data.type === "databaseInitialized") resolve(true);
|
||||
})
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,12 +26,7 @@ import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { SharedService } from "./shared-service";
|
||||
|
||||
type Config = {
|
||||
dbName: string;
|
||||
async: boolean;
|
||||
encrypted: boolean;
|
||||
init?: () => Promise<void>;
|
||||
};
|
||||
type Config = { dbName: string; async: boolean; init?: () => Promise<void> };
|
||||
|
||||
const servicePool = new Map<
|
||||
string,
|
||||
@@ -60,6 +55,7 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
if (closed) {
|
||||
console.log("Already activated. Reinitializing...");
|
||||
await service.proxy.open(
|
||||
this.config.dbName,
|
||||
this.config.async,
|
||||
this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
);
|
||||
@@ -76,24 +72,19 @@ 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({
|
||||
dbName: this.config.dbName,
|
||||
async: this.config.async,
|
||||
encrypted: this.config.encrypted,
|
||||
uri: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -89,7 +89,32 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: [],
|
||||
: [
|
||||
{
|
||||
title: "Bi-directional note linking",
|
||||
subtitle: "You can now link any 2 notes for easier referencing."
|
||||
},
|
||||
{
|
||||
title: "Tabs",
|
||||
subtitle:
|
||||
"Open multiple notes side by side for faster multi-tasking."
|
||||
},
|
||||
{
|
||||
title: "Nested notebooks",
|
||||
subtitle:
|
||||
"Create subnotebooks inside notebooks upto unlimited depth."
|
||||
},
|
||||
{
|
||||
title: "At rest encryption",
|
||||
subtitle:
|
||||
"All your data will now be stored encrypted on your device to keep you safe even if your device gets compromised."
|
||||
},
|
||||
{
|
||||
title: "App lock",
|
||||
subtitle:
|
||||
"App lock is now here on desktop/web apps! Put a lock on the app, and keep it safe from intruders."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
title: "Got it",
|
||||
icon: Checkmark,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -22,32 +22,15 @@ import {
|
||||
logger as _logger,
|
||||
logManager
|
||||
} from "@notesnook/core/dist/logger";
|
||||
import { LogMessage, NoopLogger, format } from "@notesnook/logger";
|
||||
import { LogMessage } from "@notesnook/logger";
|
||||
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
|
||||
import { ZipFile, createZipStream } from "./streams/zip-stream";
|
||||
import { createWriteStream } from "./stream-saver";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { createDialect } from "../common/sqlite";
|
||||
import { isFeatureSupported } from "./feature-check";
|
||||
|
||||
let logger: typeof _logger = new NoopLogger();
|
||||
async function initializeLogger() {
|
||||
await initialize(
|
||||
{
|
||||
dialect: (name, init) => createDialect(name, false, init),
|
||||
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
journalMode: "MEMORY",
|
||||
lockingMode: "exclusive"
|
||||
}),
|
||||
tempStore: "memory",
|
||||
synchronous: "normal",
|
||||
pageSize: 8192,
|
||||
cacheSize: -32000,
|
||||
skipInitialization: !IS_DESKTOP_APP
|
||||
},
|
||||
false
|
||||
);
|
||||
let logger: typeof _logger;
|
||||
async function initializeLogger(persistence: DatabasePersistence = "db") {
|
||||
initialize(new NNStorage("Logs", () => null, persistence), false);
|
||||
logger = _logger.scope("notesnook-web");
|
||||
}
|
||||
|
||||
@@ -64,9 +47,11 @@ async function downloadLogs() {
|
||||
return;
|
||||
}
|
||||
controller.enqueue({
|
||||
path: sanitizeFilename(log.key, { replacement: "-" }) + ".log",
|
||||
path: sanitizeFilename(log.key, { replacement: "-" }),
|
||||
data: textEncoder.encode(
|
||||
(log.logs as LogMessage[]).map((line) => format(line)).join("\n")
|
||||
(log.logs as LogMessage[])
|
||||
.map((line) => JSON.stringify(line))
|
||||
.join("\n")
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
@@ -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!
|
||||
@@ -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 {
|
||||
|
||||
@@ -63,7 +63,6 @@ import { Settings } from "../collections/settings";
|
||||
import {
|
||||
DatabaseAccessor,
|
||||
DatabaseSchema,
|
||||
RawDatabaseSchema,
|
||||
SQLiteOptions,
|
||||
changeDatabasePassword,
|
||||
createDatabase,
|
||||
@@ -75,8 +74,7 @@ import { Vaults } from "../collections/vaults";
|
||||
import { KVStorage } from "../database/kv";
|
||||
import { QueueValue } from "../utils/queue-value";
|
||||
import { Sanitizer } from "../database/sanitizer";
|
||||
import { createTriggers, dropTriggers } from "../database/triggers";
|
||||
import { NNMigrationProvider } from "../database/migrations";
|
||||
import { dropTriggers } from "../database/triggers";
|
||||
|
||||
type EventSourceConstructor = new (
|
||||
uri: string,
|
||||
@@ -181,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);
|
||||
@@ -246,10 +244,7 @@ class Database {
|
||||
await sql.raw(statement).execute(this.sql());
|
||||
}
|
||||
|
||||
await initializeDatabase(
|
||||
this.sql().withTables(),
|
||||
new NNMigrationProvider()
|
||||
);
|
||||
await initializeDatabase(this.sql().withTables());
|
||||
await this.initCollections();
|
||||
return true;
|
||||
}
|
||||
@@ -279,11 +274,10 @@ class Database {
|
||||
this.disconnectSSE();
|
||||
});
|
||||
|
||||
this._sql = (await createDatabase<RawDatabaseSchema>("notesnook", {
|
||||
...this.options.sqliteOptions,
|
||||
migrationProvider: new NNMigrationProvider(),
|
||||
onInit: (db) => createTriggers(db)
|
||||
})) as unknown as Kysely<DatabaseSchema>;
|
||||
this._sql = (await createDatabase(
|
||||
"notesnook",
|
||||
this.options.sqliteOptions
|
||||
)) as unknown as Kysely<DatabaseSchema>;
|
||||
|
||||
await this.sanitizer.init();
|
||||
|
||||
|
||||
@@ -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", "!=", 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
|
||||
|
||||
@@ -100,6 +100,9 @@ export class Attachments implements ICollection {
|
||||
|
||||
async init() {
|
||||
await this.collection.init();
|
||||
logger.debug("attachments initialized", {
|
||||
total: await this.collection.count()
|
||||
});
|
||||
}
|
||||
|
||||
async add(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -34,8 +34,7 @@ import {
|
||||
ColumnType,
|
||||
ExpressionBuilder,
|
||||
ReferenceExpression,
|
||||
Dialect,
|
||||
MigrationProvider
|
||||
Dialect
|
||||
} from "kysely";
|
||||
import {
|
||||
Attachment,
|
||||
@@ -59,6 +58,8 @@ import {
|
||||
Vault,
|
||||
isDeleted
|
||||
} from "../types";
|
||||
import { NNMigrationProvider } from "./migrations";
|
||||
import { createTriggers } from "./triggers";
|
||||
import { logger } from "../logger";
|
||||
|
||||
// type FilteredKeys<T, U> = {
|
||||
@@ -159,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>;
|
||||
@@ -257,8 +257,8 @@ const DataMappers: Partial<Record<ItemType, (row: any) => void>> = {
|
||||
}
|
||||
};
|
||||
|
||||
async function setupDatabase<Schema>(
|
||||
db: Kysely<Schema>,
|
||||
async function setupDatabase(
|
||||
db: Kysely<RawDatabaseSchema>,
|
||||
options: SQLiteOptions
|
||||
) {
|
||||
if (options.password)
|
||||
@@ -294,14 +294,11 @@ async function setupDatabase<Schema>(
|
||||
);
|
||||
}
|
||||
|
||||
export async function initializeDatabase<Schema>(
|
||||
db: Kysely<Schema>,
|
||||
migrationProvider: MigrationProvider
|
||||
) {
|
||||
export async function initializeDatabase(db: Kysely<RawDatabaseSchema>) {
|
||||
try {
|
||||
const migrator = new Migrator({
|
||||
db,
|
||||
provider: migrationProvider
|
||||
provider: new NNMigrationProvider()
|
||||
});
|
||||
const { error, results } = await migrator.migrateToLatest();
|
||||
|
||||
@@ -316,6 +313,8 @@ export async function initializeDatabase<Schema>(
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
await createTriggers(db);
|
||||
|
||||
return db;
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to initialized database.");
|
||||
@@ -336,14 +335,8 @@ export type SQLiteOptions = {
|
||||
|
||||
skipInitialization?: boolean;
|
||||
};
|
||||
export async function createDatabase<Schema>(
|
||||
name: string,
|
||||
options: SQLiteOptions & {
|
||||
migrationProvider: MigrationProvider;
|
||||
onInit?: (db: Kysely<Schema>) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
const db = new Kysely<Schema>({
|
||||
export async function createDatabase(name: string, options: SQLiteOptions) {
|
||||
const db = new Kysely<RawDatabaseSchema>({
|
||||
// log: (event) => {
|
||||
// if (event.queryDurationMillis > 5)
|
||||
// console.warn(event.query.sql, event.queryDurationMillis);
|
||||
@@ -351,8 +344,7 @@ export async function createDatabase<Schema>(
|
||||
dialect: options.dialect(name, async () => {
|
||||
await db.connection().execute(async (db) => {
|
||||
await setupDatabase(db, options);
|
||||
await initializeDatabase(db, options.migrationProvider);
|
||||
if (options.onInit) await options.onInit(db);
|
||||
await initializeDatabase(db);
|
||||
});
|
||||
}),
|
||||
plugins: [new SqliteBooleanPlugin()]
|
||||
@@ -360,8 +352,7 @@ export async function createDatabase<Schema>(
|
||||
if (!options.skipInitialization)
|
||||
await db.connection().execute(async (db) => {
|
||||
await setupDatabase(db, options);
|
||||
await initializeDatabase(db, options.migrationProvider);
|
||||
if (options.onInit) await options.onInit(db);
|
||||
await initializeDatabase(db);
|
||||
});
|
||||
|
||||
return db;
|
||||
|
||||
@@ -289,26 +289,6 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
async up(db) {
|
||||
await rebuildSearchIndex(db);
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
async up(db) {
|
||||
await db
|
||||
.updateTable("notes")
|
||||
.where("id", "in", (eb) =>
|
||||
eb
|
||||
.selectFrom("content")
|
||||
.select("noteId as id")
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("conflicted", "is", null),
|
||||
eb("conflicted", "==", false)
|
||||
])
|
||||
)
|
||||
.$castTo<string | null>()
|
||||
)
|
||||
.set({ conflicted: false })
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -28,9 +28,7 @@ import {
|
||||
format,
|
||||
ILogger
|
||||
} from "@notesnook/logger";
|
||||
import { Kysely, Migration, MigrationProvider } from "kysely";
|
||||
import { SQLiteOptions, createDatabase } from "./database";
|
||||
import { toChunks } from "./utils/array";
|
||||
import { IStorage } from "./interfaces";
|
||||
|
||||
const WEEK = 86400000 * 7;
|
||||
|
||||
@@ -40,46 +38,10 @@ const WEEK = 86400000 * 7;
|
||||
// 3. Keep 7 days of logs
|
||||
// 4. Implement functions for log retrieval & filtering
|
||||
|
||||
type SQLiteItem<T> = {
|
||||
[P in keyof T]?: T[P] | null;
|
||||
};
|
||||
|
||||
type LogMessageWithDate = LogMessage & { date: string };
|
||||
export type LogDatabaseSchema = {
|
||||
logs: SQLiteItem<LogMessageWithDate>;
|
||||
};
|
||||
|
||||
class NNLogsMigrationProvider implements MigrationProvider {
|
||||
async getMigrations(): Promise<Record<string, Migration>> {
|
||||
return {
|
||||
"1": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.createTable("logs")
|
||||
.addColumn("timestamp", "integer", (c) => c.notNull())
|
||||
.addColumn("message", "text", (c) => c.notNull())
|
||||
.addColumn("level", "integer", (c) => c.notNull())
|
||||
.addColumn("date", "text")
|
||||
.addColumn("scope", "text")
|
||||
.addColumn("extras", "text")
|
||||
.addColumn("elapsed", "integer")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("log_timestamp_index")
|
||||
.on("logs")
|
||||
.column("timestamp")
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseLogReporter {
|
||||
writer: DatabaseLogWriter;
|
||||
constructor(db: Kysely<LogDatabaseSchema>) {
|
||||
this.writer = new DatabaseLogWriter(db);
|
||||
constructor(storage: IStorage) {
|
||||
this.writer = new DatabaseLogWriter(storage);
|
||||
}
|
||||
|
||||
write(log: LogMessage) {
|
||||
@@ -88,10 +50,10 @@ class DatabaseLogReporter {
|
||||
}
|
||||
|
||||
class DatabaseLogWriter {
|
||||
private queue: LogMessageWithDate[] = [];
|
||||
private queue: Map<string, LogMessage> = new Map();
|
||||
private hasCleared = false;
|
||||
|
||||
constructor(private readonly db: Kysely<LogDatabaseSchema>) {
|
||||
constructor(private readonly storage: IStorage) {
|
||||
setInterval(() => {
|
||||
setTimeout(() => {
|
||||
if (!this.hasCleared) {
|
||||
@@ -104,86 +66,86 @@ class DatabaseLogWriter {
|
||||
}
|
||||
|
||||
push(message: LogMessage) {
|
||||
const date = new Date(message.timestamp);
|
||||
(message as LogMessageWithDate).date = `${date.getFullYear()}-${
|
||||
date.getMonth() + 1
|
||||
}-${date.getDate()}`;
|
||||
this.queue.push(message as LogMessageWithDate);
|
||||
const key = new Date(message.timestamp).toLocaleDateString();
|
||||
this.queue.set(`${key}:${message.timestamp}`, message);
|
||||
}
|
||||
|
||||
async flush() {
|
||||
if (this.queue.length === 0) return;
|
||||
const queueCopy = this.queue.slice();
|
||||
this.queue = [];
|
||||
for (const chunk of toChunks(queueCopy, 1000)) {
|
||||
await this.db.insertInto("logs").values(chunk).execute();
|
||||
}
|
||||
if (this.queue.size === 0) return;
|
||||
const queueCopy = Array.from(this.queue.entries());
|
||||
this.queue = new Map();
|
||||
|
||||
await this.storage.writeMulti(queueCopy);
|
||||
}
|
||||
|
||||
async rotate() {
|
||||
const range = Date.now() - WEEK;
|
||||
await this.db.deleteFrom("logs").where("timestamp", "<", range).execute();
|
||||
const logKeys = (await this.storage.getAllKeys()).sort();
|
||||
const keysToRemove = [];
|
||||
for (const key of logKeys) {
|
||||
const keyParts = key.split(":");
|
||||
if (keyParts.length === 1 || parseInt(keyParts[1]) < Date.now() - WEEK) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseLogManager {
|
||||
constructor(private readonly db: Kysely<LogDatabaseSchema>) {}
|
||||
constructor(private readonly storage: IStorage) {}
|
||||
|
||||
async get() {
|
||||
const logs = await this.db
|
||||
.selectFrom("logs")
|
||||
.select([
|
||||
"timestamp",
|
||||
"message",
|
||||
"level",
|
||||
"scope",
|
||||
"extras",
|
||||
"elapsed",
|
||||
"date"
|
||||
])
|
||||
.execute();
|
||||
const groupedLogs: Record<string, LogMessage[]> = {};
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
const logEntries = await this.storage.readMulti<LogMessage>(logKeys);
|
||||
const logs: Record<string, LogMessage[]> = {};
|
||||
|
||||
for (const log of logs) {
|
||||
const key = log.date!;
|
||||
if (!groupedLogs[key]) groupedLogs[key] = [];
|
||||
groupedLogs[key].push(log as LogMessage);
|
||||
for (const [logKey, log] of logEntries) {
|
||||
const keyParts = logKey.split(":");
|
||||
if (keyParts.length === 1) continue;
|
||||
|
||||
const key = keyParts[0];
|
||||
if (!logs[key]) logs[key] = [];
|
||||
logs[key].push(log);
|
||||
}
|
||||
|
||||
return Object.keys(groupedLogs)
|
||||
return Object.keys(logs)
|
||||
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
|
||||
.map((key) => ({
|
||||
key,
|
||||
logs: groupedLogs[key]?.sort((a, b) => a.timestamp - b.timestamp)
|
||||
logs: logs[key]?.sort((a, b) => a.timestamp - b.timestamp)
|
||||
}));
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await this.db.deleteFrom("logs").execute();
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
await this.storage.removeMulti(logKeys);
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
await this.db.deleteFrom("logs").where("date", "==", key).execute();
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
const keysToRemove = [];
|
||||
for (const logKey of logKeys) {
|
||||
const keyParts = logKey.split(":");
|
||||
if (keyParts.length === 1) continue;
|
||||
const currKey = keyParts[0];
|
||||
if (currKey === key) keysToRemove.push(logKey);
|
||||
}
|
||||
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize(
|
||||
options: SQLiteOptions,
|
||||
disableConsoleLogs?: boolean
|
||||
) {
|
||||
const db = await createDatabase<LogDatabaseSchema>("notesnook-logs", {
|
||||
...options,
|
||||
migrationProvider: new NNLogsMigrationProvider()
|
||||
});
|
||||
|
||||
const reporters: ILogReporter[] = [new DatabaseLogReporter(db)];
|
||||
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
|
||||
reporters.push(consoleReporter);
|
||||
logger = new Logger({
|
||||
reporter: combineReporters(reporters),
|
||||
lastTime: Date.now()
|
||||
});
|
||||
logManager = new DatabaseLogManager(db);
|
||||
function initialize(storage: IStorage, disableConsoleLogs?: boolean) {
|
||||
if (storage) {
|
||||
const reporters: ILogReporter[] = [new DatabaseLogReporter(storage)];
|
||||
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
|
||||
reporters.push(consoleReporter);
|
||||
logger = new Logger({
|
||||
reporter: combineReporters(reporters),
|
||||
lastTime: Date.now()
|
||||
});
|
||||
logManager = new DatabaseLogManager(storage);
|
||||
}
|
||||
}
|
||||
|
||||
let logger: ILogger = new NoopLogger();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -22,7 +22,9 @@ import {
|
||||
getFontById,
|
||||
getTableOfContents,
|
||||
TiptapOptions,
|
||||
usePermissionHandler
|
||||
Toolbar,
|
||||
usePermissionHandler,
|
||||
useTiptap
|
||||
} from "@notesnook/editor";
|
||||
import { toBlobURL } from "@notesnook/editor/dist/utils/downloader";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -44,13 +46,13 @@ import {
|
||||
useTabContext,
|
||||
useTabStore
|
||||
} from "../hooks/useTabStore";
|
||||
import { EventTypes, postAsyncWithTimeout, Settings } from "../utils";
|
||||
import { pendingSaveRequests } from "../utils/pending-saves";
|
||||
import { EmotionEditorToolbarTheme } from "../theme-factory";
|
||||
import { EventTypes, postAsyncWithTimeout, randId, Settings } from "../utils";
|
||||
import Header from "./header";
|
||||
import StatusBar from "./statusbar";
|
||||
import Tags from "./tags";
|
||||
import TiptapEditorWrapper from "./tiptap";
|
||||
import Title from "./title";
|
||||
import { pendingSaveRequests } from "../utils/pending-saves";
|
||||
|
||||
globalThis.toBlobURL = toBlobURL as typeof globalThis.toBlobURL;
|
||||
|
||||
@@ -75,8 +77,6 @@ const Tiptap = ({
|
||||
const biometryEnrolled = useTabStore((state) => state.biometryEnrolled);
|
||||
const editorRoot = useRef<HTMLDivElement>(null);
|
||||
const isFocusedRef = useRef<boolean>(false);
|
||||
const [undo, setUndo] = useState(false);
|
||||
const [redo, setRedo] = useState(false);
|
||||
tabRef.current = tab;
|
||||
|
||||
function restoreNoteSelection(state?: NoteState) {
|
||||
@@ -232,11 +232,14 @@ const Tiptap = ({
|
||||
settings.dateFormat,
|
||||
settings.timeFormat,
|
||||
settings.markdownShortcuts,
|
||||
tab.id,
|
||||
tick
|
||||
tick,
|
||||
tab.id
|
||||
]);
|
||||
|
||||
const _editor = useTiptap(tiptapOptions, [tiptapOptions]);
|
||||
|
||||
const update = useCallback(() => {
|
||||
editors[tabRef.current.id]?.commands.setTextSelection(0);
|
||||
setTick((tick) => tick + 1);
|
||||
globalThis.editorControllers[tabRef.current.id]?.setTitlePlaceholder(
|
||||
"Note title"
|
||||
@@ -260,6 +263,7 @@ const Tiptap = ({
|
||||
const controllerRef = useRef(controller);
|
||||
|
||||
globalThis.editorControllers[tab.id] = controller;
|
||||
globalThis.editors[tab.id] = _editor;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!getContentDiv().parentElement) {
|
||||
@@ -423,8 +427,8 @@ const Tiptap = ({
|
||||
onDoubleClick={onClickEmptyArea}
|
||||
>
|
||||
<Header
|
||||
hasRedo={redo}
|
||||
hasUndo={undo}
|
||||
hasRedo={_editor?.can().redo() || false}
|
||||
hasUndo={_editor?.can().undo() || false}
|
||||
settings={settings}
|
||||
noHeader={settings.noHeader || false}
|
||||
/>
|
||||
@@ -474,8 +478,7 @@ const Tiptap = ({
|
||||
<p
|
||||
style={{
|
||||
width: "90%",
|
||||
fontSize: "0.9rem",
|
||||
color: colors.primary.paragraph
|
||||
fontSize: "0.9rem"
|
||||
}}
|
||||
>
|
||||
<ol>
|
||||
@@ -855,23 +858,24 @@ const Tiptap = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TiptapEditorWrapper
|
||||
key={tick + tab.id + "-editor"}
|
||||
options={tiptapOptions}
|
||||
settings={settings}
|
||||
onEditorUpdate={(editor) => {
|
||||
if (!editor) {
|
||||
setUndo(false);
|
||||
setRedo(false);
|
||||
}
|
||||
if (undo !== editor.can().undo()) {
|
||||
setUndo(editor.can().undo());
|
||||
}
|
||||
if (redo !== editor.can().redo()) {
|
||||
setRedo(editor.can().redo());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{tab.locked ? null : (
|
||||
<EmotionEditorToolbarTheme>
|
||||
<Toolbar
|
||||
className="theme-scope-editorToolbar"
|
||||
sx={{
|
||||
display: settings.noToolbar ? "none" : "flex",
|
||||
overflowY: "hidden",
|
||||
minHeight: "50px",
|
||||
backgroundColor: "red"
|
||||
}}
|
||||
editor={_editor}
|
||||
location="bottom"
|
||||
tools={[...settings.tools]}
|
||||
defaultFontFamily={settings.fontFamily}
|
||||
defaultFontSize={settings.fontSize}
|
||||
/>
|
||||
</EmotionEditorToolbarTheme>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Editor, TiptapOptions, Toolbar, useTiptap } from "@notesnook/editor";
|
||||
import { useTabContext } from "../hooks/useTabStore";
|
||||
import { EmotionEditorToolbarTheme } from "../theme-factory";
|
||||
import { Settings } from "../utils";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function TiptapEditorWrapper(props: {
|
||||
options: Partial<TiptapOptions>;
|
||||
onEditorUpdate: (editor: Editor) => void;
|
||||
settings: Settings;
|
||||
}) {
|
||||
const tab = useTabContext();
|
||||
const editor = useTiptap(props.options, [props.options]);
|
||||
globalThis.editors[tab.id] = editor;
|
||||
|
||||
useEffect(() => {
|
||||
props.onEditorUpdate(editor);
|
||||
}, [editor, props]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tab.locked ? null : (
|
||||
<EmotionEditorToolbarTheme>
|
||||
<Toolbar
|
||||
className="theme-scope-editorToolbar"
|
||||
sx={{
|
||||
display: props.settings.noToolbar ? "none" : "flex",
|
||||
overflowY: "hidden",
|
||||
minHeight: "50px",
|
||||
backgroundColor: "red"
|
||||
}}
|
||||
editor={editor}
|
||||
location="bottom"
|
||||
tools={[...props.settings.tools]}
|
||||
defaultFontFamily={props.settings.fontFamily}
|
||||
defaultFontSize={props.settings.fontSize}
|
||||
/>
|
||||
</EmotionEditorToolbarTheme>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -355,13 +355,13 @@ export function useEditorController({
|
||||
}
|
||||
|
||||
scrollTo?.(noteState?.top || 0);
|
||||
|
||||
countWords(0);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "native:html":
|
||||
if (htmlContentRef.current === value) break;
|
||||
htmlContentRef.current = value;
|
||||
logger("info", "LOADING NOTE HTML");
|
||||
if (!editor) break;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -118,9 +118,7 @@ function errorLogLevelFactory(level: LogLevel, config: LoggerConfig) {
|
||||
extras:
|
||||
error instanceof Error
|
||||
? { ...extras, fallbackMessage }
|
||||
: error
|
||||
? { ...extras, error }
|
||||
: extras,
|
||||
: { ...extras, error },
|
||||
scope: config.scope,
|
||||
elapsed: now - config.lastTime
|
||||
});
|
||||
|
||||
@@ -294,7 +294,6 @@ export const THEME_SCOPES: readonly (keyof ThemeScopes)[] = [
|
||||
"editor",
|
||||
"editorToolbar",
|
||||
"editorSidebar",
|
||||
"titleBar",
|
||||
"dialog",
|
||||
"navigationMenu",
|
||||
"contextMenu",
|
||||
|
||||
Reference in New Issue
Block a user