Compare commits

..

10 Commits

Author SHA1 Message Date
Ammar Ahmed
a45b66d449 mobile: release v3.0.9 2024-06-01 14:14:01 +05:00
Ammar Ahmed
dd67b1a803 mobile: fix login 2fa sheet gets blocked by keyboard 2024-06-01 13:45:28 +05:00
Ammar Ahmed
00568ae839 mobile: add back undo/redo buttons 2024-06-01 13:21:27 +05:00
Abdullah Atta
2b2ea5717c web: bring back undo/redo buttons 2024-06-01 13:21:27 +05:00
Abdullah Atta
5672de8565 desktop: allow using native titlebar on desktop app (#5826) 2024-06-01 13:21:05 +05:00
Ammar Ahmed
1c70ad8cec web: fix tags & colors not included in imported notes (#5801)
* web: include tags and colors in imports

* web: refactors

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-06-01 11:40:16 +05:00
Abdullah Atta
0c344a2146 core: fix sqlite hangs on restoring notebook 2024-06-01 11:28:40 +05:00
Yoonjae Choi
38f69b514c web: fix readonly locked notes opening as editable (#5731)
Signed-off-by: Yoonjae Choi <dbswo9795@email.com>
2024-06-01 11:27:59 +05:00
Abdullah Atta
b502f6a08f web: fix CVE-2024-4367 2024-06-01 11:26:43 +05:00
Abdullah Atta
d68c3269d3 editor: fix extra padding around image 2024-06-01 11:26:28 +05:00
34 changed files with 557 additions and 2833 deletions

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { initTRPC } from "@trpc/server";
import { z } from "zod";
import { dialog, nativeTheme, Notification, shell } from "electron";
import { app, dialog, nativeTheme, Notification, shell } from "electron";
import { AutoLaunch } from "../utils/autolaunch";
import { config, DesktopIntegration } from "../utils/config";
import { bringToFront } from "../utils/bring-to-front";
@@ -152,6 +152,10 @@ export const osIntegrationRouter = t.router({
await rm(input);
}),
restart: t.procedure.query(() => {
app.relaunch();
app.exit();
}),
showNotification: t.procedure
.input(NotificationOptions)
.query(({ input }) => {
@@ -191,7 +195,10 @@ export const osIntegrationRouter = t.router({
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
if (windowControlsIconColor) {
config.windowControlsIconColor = windowControlsIconColor;
if (process.platform === "win32")
if (
process.platform === "win32" &&
!config.desktopSettings.nativeTitlebar
)
globalThis.window?.setTitleBarOverlay({
symbolColor: windowControlsIconColor
});

View File

@@ -84,17 +84,21 @@ async function createWindow() {
format: process.platform === "win32" ? "ico" : "png"
}),
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
},
...(config.desktopSettings.nativeTitlebar
? {}
: {
titleBarStyle: "hidden",
frame: process.platform === "win32" || process.platform === "darwin",
titleBarOverlay: {
height: 37,
color: "#00000000",
symbolColor: config.windowControlsIconColor
},
trafficLightPosition: {
x: 16,
y: 12
}
}),
webPreferences: {
zoomFactor: config.zoomFactor,

View File

@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
autoStart: z.boolean().optional(),
startMinimized: z.boolean().optional(),
minimizeToSystemTray: z.boolean().optional(),
closeToSystemTray: z.boolean().optional()
closeToSystemTray: z.boolean().optional(),
nativeTitlebar: z.boolean().optional()
});
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
@@ -35,7 +36,8 @@ export const config = {
autoStart: false,
startMinimized: false,
minimizeToSystemTray: false,
closeToSystemTray: false
closeToSystemTray: false,
nativeTitlebar: false
},
privacyMode: false,
isSpellCheckerEnabled: true,

View File

@@ -355,6 +355,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
component: () => <AttachmentDialog note={note} />,
keyboardHandlerDisabled: true
});
};

View File

@@ -17,12 +17,16 @@ 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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import Sync from "../../services/sync";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import SheetProvider from "../sheet-provider";
@@ -34,11 +38,6 @@ import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { useSettingStore } from "../../stores/use-setting-store";
import { eUserLoggedIn } from "../../utils/events";
import { useUserStore } from "../../stores/use-user-store";
import Sync from "../../services/sync";
import { Notice } from "../ui/notice";
const LoginSteps = {
emailAuth: 1,
@@ -184,7 +183,11 @@ export const Login = ({ changeMode }) => {
defaultValue={email.current}
editable={step === LoginSteps.emailAuth && !loading}
onSubmit={() => {
passwordInputRef.current?.focus();
if (step === LoginSteps.emailAuth) {
login();
} else {
passwordInputRef.current?.focus();
}
}}
/>
@@ -243,6 +246,7 @@ export const Login = ({ changeMode }) => {
width: 250,
borderRadius: 100
}}
height={50}
fontSize={SIZE.md}
type="accent"
title={!loading ? "Continue" : null}

View File

@@ -37,6 +37,7 @@ import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
import { ScrollView } from "react-native-actions-sheet";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const { colors } = useThemeColors();
@@ -143,7 +144,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
return (
<View>
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
>
<View
style={{
alignItems: "center",
@@ -214,6 +218,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
code.current = value;
//onNext();
}}
onSubmitEditing={onNext}
caretHidden
inputStyle={{
fontSize: SIZE.lg,
@@ -225,6 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
keyboardType={
currentMethod.method === "recoveryCode" ? "default" : "numeric"
}
enablesReturnKeyAutomatically
containerStyle={{
height: 60,
borderWidth: 0,
@@ -297,7 +303,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
</>
)}
</View>
</View>
</ScrollView>
);
};

View File

@@ -49,7 +49,8 @@ export const Card = ({ color }: { color?: string }) => {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 0
paddingHorizontal: 0,
width: "100%"
}}
>
<View
@@ -91,7 +92,6 @@ export const Card = ({ color }: { color?: string }) => {
<View
style={{
marginLeft: 10,
flexShrink: 1,
marginRight: 10
}}
>

View File

@@ -25,7 +25,8 @@ import {
DimensionValue,
TextStyle,
View,
ViewStyle
ViewStyle,
useWindowDimensions
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useUserStore } from "../../../stores/use-user-store";
@@ -92,6 +93,9 @@ export const Button = ({
});
const textColor = buttonType?.text ? buttonType.text : text;
const { fontScale } = useWindowDimensions();
const growFactor = 1 + (fontScale - 1) / 10;
const Component = bold ? Heading : Paragraph;
return (
@@ -117,8 +121,11 @@ export const Button = ({
customOpacity={buttonType?.opacity}
customAlpha={buttonType?.alpha}
style={{
height: height,
width: (width as DimensionValue) || undefined,
height: typeof height === "number" ? height * growFactor : height,
width:
typeof width === "number"
? width * growFactor
: (width as DimensionValue) || undefined,
paddingHorizontal: 12,
borderRadius: 5,
alignSelf: "center",

View File

@@ -21,11 +21,12 @@ import { VariantsWithStaticColors, useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback } from "react";
import {
ColorValue,
PressableStateCallbackType,
Pressable as RNPressable,
PressableProps as RNPressableProps,
PressableStateCallbackType,
View,
ViewStyle
ViewStyle,
useWindowDimensions
} from "react-native";
import {
RGB_Linear_Shade,
@@ -256,6 +257,8 @@ export const Pressable = ({
? 1
: colorOpacity;
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
const { fontScale } = useWindowDimensions();
const growFactor = 1 + (fontScale - 1) / 8;
const getStyle = useCallback(
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
@@ -276,7 +279,13 @@ export const Pressable = ({
: borderColor || "transparent",
borderWidth: borderWidth
},
style
style,
{
height:
typeof style.height === "number"
? style.height * growFactor
: style.height
}
],
[
alpha,
@@ -288,7 +297,8 @@ export const Pressable = ({
borderSelectedColor,
borderColor,
borderWidth,
style
style,
growFactor
]
);

View File

@@ -111,7 +111,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3016
versionCode 3017
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -1,3 +1,4 @@
- Bring back undo/redo buttons in editor
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -1015,7 +1015,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2105;
CURRENT_PROJECT_VERSION = 2106;
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.9;
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 = 2106;
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.9;
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 = 2106;
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.9;
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 = 2106;
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.9;
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 = 2106;
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.9;
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 = 2106;
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.9;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

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

3
apps/web/.gitignore vendored
View File

@@ -30,5 +30,4 @@ dist
public/workbox
scripts/secrets
test-results
.swc
public/models
.swc

View File

@@ -1,128 +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 { describe, bench } from "vitest";
import { pipeline } from "@xenova/transformers";
import path from "path";
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
Migrating to SQLite
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
The clients should perform predictably regardless of how much data you have.
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
With v3 we have acheived both of these goals.
Fixing the Sync
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
Building everything on top of timestamps meant resumability during sync could never be stable.
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
Features, Features
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
Note linking
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
Tabs
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
Nested notebooks
At rest encryption
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
App lock
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
At Notesnook, we take your security & privacy very seriously.
Export attachments with notes
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
User profile
Custom colors for note organization
Fixed colors were nice but not nicer than custom colors.
Customizable side bar
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
Callouts
Beta release channels
Nested notebooks/topics
Note linking
Tabs
Table of Contents
Customizable/sortable sidebar
User profile
Custom colors
Yearly reminders
Callouts in editor
Migration to SQLite
New sync system
At rest encryption
app lock
export attachments with notes
.ccccccccccxccccccc`;
describe("benchmarking embeddings", async () => {
const models = [
["Xenova/jina-embeddings-v2-small-en", 87],
["Snowflake/snowflake-arctic-embed-s", 41],
["Snowflake/snowflake-arctic-embed-m", 20],
["Snowflake/snowflake-arctic-embed-xs", 62],
// "Alibaba-NLP/gte-base-en-v1.5",
// "andersonbcdefg/bge-small-4096"
["Xenova/GIST-small-Embedding-v0", 57],
["Xenova/GIST-all-MiniLM-L6-v2", 88],
// "Xenova/all-MiniLM-L12-v2" // 96
["Xenova/NoInstruct-small-Embedding-v0", 40],
["TaylorAI/bge-micro-v2", 100],
// "Snowflake/snowflake-arctic-embed-m-long" // 19
// "Xenova/e5-small-v2" // 67
// "nomic-ai/nomic-embed-text-v1.5",
// "Xenova/bert-base-uncased",
["TaylorAI/gte-tiny", 88],
["Xenova/gte-small", 63]
];
for (const provider of ["cpu"]) {
for (const [model, rank] of models) {
const extractor = await pipeline("feature-extraction", model, {
// progress_callback: console.log,
device: provider,
dtype: "q8",
cache_dir: path.join(__dirname, "..", "..", "public", "models")
});
console.log(model, extractor.tokenizer.model_max_length);
extractor.tokenizer.model_max_length = 512;
bench(`${rank}. ${model} (${provider})`, async () => {
const output = await extractor(text, {
pooling: extractor.tokenizer._tokenizer_config.cls_token
? "cls"
: "mean"
});
});
}
}
});
// console.time("extraction");
// const output = await extractor(text);
// console.timeEnd("extraction");
// console.log(output.data.length);

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,6 @@
"@theme-ui/core": "^0.16.1",
"@trpc/client": "10.38.3",
"@trpc/react-query": "10.38.3",
"@xenova/transformers": "github:xenova/transformers.js#v3",
"@zip.js/zip.js": "^2.7.32",
"async-mutex": "^0.4.0",
"axios": "^1.3.4",
@@ -61,7 +60,6 @@
"libsodium-wrappers": "^0.7.13",
"mac-scrollbar": "^0.13.5",
"marked": "^4.1.0",
"onnxruntime-node": "^1.18.0",
"pdfjs-dist": "3.6.172",
"phone": "^3.1.14",
"platform": "^1.3.6",
@@ -132,9 +130,6 @@
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0"
},
"overrides": {
"onnxruntime-node": "^1.18.0"
},
"scripts": {
"start": "cross-env PLATFORM=web vite",
"start:desktop": "cross-env PLATFORM=desktop vite",

View File

@@ -44,272 +44,11 @@ import { User } from "@notesnook/core";
import { LegacyBackupFile } from "@notesnook/core";
import { useEditorStore } from "../stores/editor-store";
import { formatDate } from "@notesnook/core/dist/utils/date";
import {
pipeline,
AutoModel,
env,
Tensor,
mean_pooling,
FeatureExtractionPipeline
} from "@xenova/transformers";
import { toChunks } from "@notesnook/core/dist/utils/array";
env.allowLocalModels = true;
console.log(env);
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
Migrating to SQLite
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
The clients should perform predictably regardless of how much data you have.
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
With v3 we have acheived both of these goals.
Fixing the Sync
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
Building everything on top of timestamps meant resumability during sync could never be stable.
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
Features, Features
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
Note linking
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
Tabs
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
Nested notebooks
At rest encryption
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
App lock
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
At Notesnook, we take your security & privacy very seriously.
Export attachments with notes
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
User profile
Custom colors for note organization
Fixed colors were nice but not nicer than custom colors.
Customizable side bar
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
Callouts
Beta release channels
Nested notebooks/topics
Note linking
Tabs
Table of Contents
Customizable/sortable sidebar
User profile
Custom colors
Yearly reminders
Callouts in editor
Migration to SQLite
New sync system
At rest encryption
app lock
export attachments with notes.`;
const average = (array: number[]) =>
array.reduce((a, b) => a + b) / array.length;
function createTensorWithBatchSize(opt: [number, number]) {
const size = opt.reduce((n, i) => n * i, 1);
return new Tensor("int64", new BigInt64Array(size).fill(BigInt(1)), opt);
}
function createBatch(batchSize: number, sequenceLength: number) {
const tensor = createTensorWithBatchSize([batchSize, sequenceLength]);
return {
input_ids: tensor,
attention_mask: tensor
};
}
function chunkify(tokens: string[], chunkSize: number, overlap: number) {
const chunks = [];
const totalParts = Math.ceil(tokens.length / chunkSize);
for (let i = 0; i < totalParts; ++i) {
const start = i === 0 ? 0 : i * chunkSize - i * overlap;
const end = start + chunkSize;
chunks.push(tokens.slice(start, end));
}
return chunks;
}
/**
*
* Helper function for padding values of an object, which are each arrays.
* NOTE: No additional checks are made here for validity of arguments.
* @param {Record<string, any[]>} item The input object.
* @param {number} length The length to pad to.
* @param {(key: string) => any} value_fn Determine the value to fill the array, based on its key.
* @param {string} side Which side to pad the array.
* @private
*/
function padHelper(
item: Record<string, any[]>,
length: number,
value_fn: (key: string) => any,
side: string
) {
for (const key of Object.keys(item)) {
const diff = length - item[key].length;
const value = value_fn(key);
const padData = new Array(diff).fill(value);
item[key] =
side === "right"
? [...item[key], ...padData]
: [...padData, ...item[key]];
}
}
function tokenizeInput(
pipeline: FeatureExtractionPipeline,
options: { max_length: number }
) {
const { tokenizer } = pipeline;
const { max_length } = options;
const encoded = tokenizer._encode_text(text) ?? [];
const encodedTokens = chunkify(encoded, max_length - 12, 20).map((chunk) => {
const { tokens, token_type_ids } = tokenizer.post_processor._call(
chunk,
[],
{ add_special_tokens: true }
);
const input_ids = tokenizer.model.convert_tokens_to_ids(tokens);
return {
input_ids,
attention_mask: new Array(input_ids.length).fill(1),
token_type_ids: token_type_ids || []
};
});
for (let i = 0; i < encodedTokens.length; ++i) {
if (encodedTokens[i].input_ids.length === max_length) {
continue;
} else if (encodedTokens[i].input_ids.length > max_length) {
throw new Error("Truncation is not allowed.");
} else {
// t.length < max_length
// possibly pad
padHelper(
encodedTokens[i],
max_length,
(key) => (key === "input_ids" ? tokenizer.pad_token_id : 0),
tokenizer.padding_side
);
}
}
const dims = [encodedTokens.length, encodedTokens[0].input_ids.length];
const input: Record<string, Tensor> = {};
for (const key of Object.keys(encodedTokens[0])) {
input[key] = new Tensor(
"int64",
BigInt64Array.from(encodedTokens.flatMap((x) => x[key]).map(BigInt)),
dims
);
}
return input;
}
async function generateEmbeddings(
pipeline: FeatureExtractionPipeline,
input: Record<string, Tensor>
) {
const { tokenizer, model } = pipeline;
const outputs = await model._call(input);
let result =
outputs.last_hidden_state ?? outputs.logits ?? outputs.token_embeddings;
const pooling: "mean" | "cls" = tokenizer._tokenizer_config.cls_token
? "cls"
: "mean";
if (pooling === "mean") {
result = mean_pooling(result, input.attention_mask);
} else if (pooling === "cls") {
result = result.slice(null, 0);
}
return result;
}
export const CREATE_BUTTON_MAP = {
notes: {
title: "Add a note",
onClick: async () => {
const models = [
// ["Xenova/jina-embeddings-v2-small-en", 87]
["Snowflake/snowflake-arctic-embed-s", 41],
// ["Snowflake/snowflake-arctic-embed-m", 20],
["Snowflake/snowflake-arctic-embed-xs", 62],
// ["Xenova/GIST-small-Embedding-v0", 57],
["Xenova/GIST-all-MiniLM-L6-v2", 88],
// ["Xenova/NoInstruct-small-Embedding-v0", 40],
["TaylorAI/bge-micro-v2", 100]
// ["TaylorAI/gte-tiny", 88],
// ["Xenova/gte-small", 63]
] as const;
// env.backends.onnx.wasm.proxy = false;
// env.backends.onnx.webgpu.profiling = {
// mode: "default"
// };
// env.webgpu.profiling = {
// mode: "default"
// };
console.log(env);
for (const provider of ["webgpu"] as const) {
if (provider === "webgpu") env.backends.onnx.wasm.proxy = false;
else env.backends.onnx.wasm.proxy = true;
for (const [modelId, rank] of models) {
console.log("loading model", modelId);
const extractor = await pipeline("feature-extraction", modelId, {
// progress_callback: console.log,
device: provider,
dtype: "fp32",
cache_dir: "/models",
local_files_only: true
// session_options: {
// // enableGraphCapture: true
// }
});
const input = tokenizeInput(extractor, { max_length: 512 });
// console.time(`[warming up] ${rank}. ${modelId} (${provider})`);
for (let i = 0; i < 3; ++i) {
console.time(`${rank}. ${modelId} (${provider})`);
await generateEmbeddings(extractor, input);
console.timeEnd(`${rank}. ${modelId} (${provider})`);
}
// console.timeEnd(`[warming up] ${rank}. ${modelId} (${provider})`);
// const timings: number[] = [];
// for (let i = 0; i < 10; ++i) {
// const now = performance.now();
// await generateEmbeddings(extractor, input);
// timings.push(performance.now() - now);
// }
// console.log(
// `[bench] ${rank}. ${modelId} (${provider})`,
// `avg: ${average(timings)}ms`,
// `min: ${Math.min(...timings)}ms`,
// `max: ${Math.max(...timings)}ms`
// );
}
}
} // useEditorStore.getState().newSession()
onClick: () => useEditorStore.getState().newSession()
},
notebooks: {
title: "Create a notebook",

View File

@@ -34,9 +34,11 @@ import {
Publish,
Published,
Readonly,
Redo,
Search,
TableOfContents,
Trash,
Undo,
Unlock
} from "../icons";
import { ScrollContainer } from "@notesnook/ui";
@@ -79,8 +81,8 @@ export function EditorActionBar() {
const activeSession = useEditorStore((store) =>
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
);
const editor = useEditorManager((store) =>
activeSession?.id ? store.editors[activeSession?.id]?.editor : undefined
const editorManager = useEditorManager((store) =>
activeSession?.id ? store.editors[activeSession?.id] : undefined
);
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
const monographs = useMonographStore((store) => store.monographs);
@@ -88,6 +90,18 @@ export function EditorActionBar() {
activeSession && db.monographs.isPublished(activeSession.id);
const tools = [
{
title: "Undo",
icon: Undo,
enabled: editorManager?.canUndo,
onClick: () => editorManager?.editor?.undo()
},
{
title: "Redo",
icon: Redo,
enabled: editorManager?.canRedo,
onClick: () => editorManager?.editor?.redo()
},
{
title: isNotePublished ? "Published" : "Publish",
icon: isNotePublished ? Published : Publish,
@@ -154,7 +168,7 @@ export function EditorActionBar() {
activeSession.type !== "locked" &&
activeSession.type !== "diff" &&
activeSession.type !== "conflicted",
onClick: editor?.startSearch
onClick: editorManager?.editor?.startSearch
},
{
title: "Properties",

View File

@@ -127,7 +127,7 @@ export default function TabsView() {
return (
<>
{IS_DESKTOP_APP ? (
{!hasNativeTitlebar ? (
ReactDOM.createPortal(
<EditorActionBar />,
document.getElementById("titlebar-portal-container")!
@@ -811,7 +811,7 @@ function UnlockNoteView(props: UnlockNoteViewProps) {
throw new Error("note with this id does not exist.");
useEditorStore.getState().addSession({
type: "default",
type: session.note.readonly ? "readonly" : "default",
locked: true,
id: session.id,
note: session.note,

View File

@@ -340,9 +340,9 @@ export class Lightbox extends React.Component<LightboxProps> {
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end",
height: IS_DESKTOP_APP ? TITLE_BAR_HEIGHT : "auto",
height: !hasNativeTitlebar ? TITLE_BAR_HEIGHT : "auto",
pr:
IS_DESKTOP_APP && getPlatform() !== "darwin"
!hasNativeTitlebar && getPlatform() !== "darwin"
? "calc(100vw - env(titlebar-area-width))"
: 0
}}

View File

@@ -239,6 +239,10 @@ export function PdfPreview(props: PdfPreviewProps) {
onZoom={(e) => {
if (hash) setPDFConfig(hash, { scale: e.scale });
}}
transformGetDocumentParams={(options) => {
(options as any).isEvalSupported = false;
return options;
}}
// onDocumentAskPassword={(e) => {
// e.verifyPassword("failed");
// }}

View File

@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SettingsGroup } from "./types";
import { useStore as useSettingStore } from "../../stores/setting-store";
import { showToast } from "../../utils/toast";
import { desktop } from "../../common/desktop-bridge";
export const DesktopIntegrationSettings: SettingsGroup[] = [
{
@@ -126,6 +128,42 @@ export const DesktopIntegrationSettings: SettingsGroup[] = [
})
}
]
},
{
key: "use-native-titlebar",
title: "Use native titlebar",
description:
"Use native OS titlebar instead of replacing it with a custom one. Requires app restart for changes to take effect.",
onStateChange: (listener) =>
useSettingStore.subscribe(
(s) => s.desktopIntegrationSettings,
listener
),
components: [
{
type: "toggle",
isToggled: () =>
!!useSettingStore.getState().desktopIntegrationSettings
?.nativeTitlebar,
toggle: () => {
useSettingStore.getState().setDesktopIntegration({
nativeTitlebar:
!useSettingStore.getState().desktopIntegrationSettings
?.nativeTitlebar
});
showToast(
"success",
"Restart the app for changes to take effect.",
[
{
text: "Restart now",
onClick: () => desktop?.integration.restart.query()
}
]
);
}
}
]
}
]
}

View File

@@ -32,6 +32,7 @@ declare global {
var IS_BETA: boolean;
var APP_TITLE: string;
var IS_THEME_BUILDER: boolean;
var hasNativeTitlebar: boolean;
interface AuthenticationExtensionsClientInputs {
prf?: {

View File

@@ -50,6 +50,9 @@ export function useWindowControls() {
isMaximized,
isFullscreen,
hasNativeWindowControls:
!IS_DESKTOP_APP || getPlatform() === "darwin" || getPlatform() === "win32"
!IS_DESKTOP_APP ||
hasNativeTitlebar ||
getPlatform() === "darwin" ||
getPlatform() === "win32"
};
}

View File

@@ -26,6 +26,7 @@ import { register } from "./utils/stream-saver/mitm";
import { getServiceWorkerVersion } from "./utils/version";
import { ErrorBoundary, ErrorComponent } from "./components/error-boundary";
import { TitleBar } from "./components/title-bar";
import { desktop } from "./common/desktop-bridge";
renderApp();
@@ -34,6 +35,12 @@ async function renderApp() {
if (!rootElement) return;
const root = createRoot(rootElement);
window.hasNativeTitlebar =
!IS_DESKTOP_APP ||
!!(await desktop?.integration.desktopIntegration
.query()
?.then((s) => s.nativeTitlebar));
try {
const { component, props, path } = await init();
@@ -47,7 +54,7 @@ async function renderApp() {
root.render(
<>
{IS_DESKTOP_APP ? <TitleBar /> : null}
{hasNativeTitlebar ? null : <TitleBar />}
<ErrorBoundary>
<BaseThemeProvider
onRender={() => document.getElementById("splash")?.remove()}
@@ -63,7 +70,7 @@ async function renderApp() {
} catch (e) {
root.render(
<>
{IS_DESKTOP_APP ? <TitleBar /> : null}
{hasNativeTitlebar ? null : <TitleBar />}
<ErrorComponent
error={e}
resetErrorBoundary={() => window.location.reload()}

View File

@@ -123,8 +123,8 @@ const decoder = new TextDecoder();
*/
class KeyStore extends BaseStore<KeyStore> {
#secretStore: IKVStore;
#metadataStore: IKVStore;
#secretStore!: IKVStore;
#metadataStore!: IKVStore;
#keyId = "key";
#wrappingKeyId = "wrappingKey";
#key?: CryptoKey;
@@ -134,25 +134,25 @@ class KeyStore extends BaseStore<KeyStore> {
isLocked = false;
constructor(
dbName: string,
private readonly dbName: string,
setState: SetState<KeyStore>,
get: GetState<KeyStore>
) {
super(setState, get);
this.#metadataStore =
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
? new IndexedDBKVStore(`${dbName}-metadata`, "metadata")
: new MemoryKVStore();
this.#secretStore =
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
? new IndexedDBKVStore(`${dbName}-secrets`, "secrets")
: new MemoryKVStore();
}
activeCredentials = () => this.get().credentials.filter((c) => c.active);
init = async () => {
this.#metadataStore =
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
? new IndexedDBKVStore(`${this.dbName}-metadata`, "metadata")
: new MemoryKVStore();
this.#secretStore =
isFeatureSupported("indexedDB") && isFeatureSupported("clonableCryptoKey")
? new IndexedDBKVStore(`${this.dbName}-secrets`, "secrets")
: new MemoryKVStore();
const credentials = await this.getCredentials();
const secrets = Object.fromEntries(
await this.#secretStore.entries<EncryptedData>()

View File

@@ -90,6 +90,7 @@ export type ReadonlyEditorSession = BaseEditorSession & {
content?: NoteContent<false>;
color?: string;
tags?: Tag[];
locked?: boolean;
};
export type DeletedEditorSession = BaseEditorSession & {

View File

@@ -89,6 +89,21 @@ async function processAttachment(
attachments[name] = { ...cipherData, key };
}
const colorMap: Record<string, string | undefined> = {
default: undefined,
teal: "#00897B",
red: "#D32F2F",
purple: "#7B1FA2",
blue: "#1976D2",
cerulean: "#03A9F4",
pink: "#C2185B",
brown: "#795548",
gray: "#9E9E9E",
green: "#388E3C",
orange: "#FFA000",
yellow: "#FFC107"
};
async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
const note = await fileToJson<Note>(entry);
for (const attachment of note.attachments || []) {
@@ -121,8 +136,49 @@ async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
content: { type: "tiptap", data: note.content?.data },
notebooks: []
});
if (!noteId) return;
for (const tag of note.tags || []) {
const tagId =
(await db.tags.find(tag))?.id ||
(await db.tags.add({
title: tag
}));
await db.relations.add(
{
id: tagId,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
}
const colorCode = note.color ? colorMap[note.color] : undefined;
if (colorCode) {
const colorId =
(await db.colors.find(colorCode))?.id ||
(await db.colors.add({
colorCode: colorCode,
title: note.color
}));
await db.relations.add(
{
id: colorId,
type: "color"
},
{
id: noteId,
type: "note"
}
);
}
for (const nb of notebooks) {
const notebookId = await importNotebook(nb).catch(() => undefined);
if (!notebookId) continue;

View File

@@ -0,0 +1,4 @@
- Bring back undo/redo buttons in editor
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -245,10 +245,10 @@ export class Notebooks implements ICollection {
const query = tr
.withRecursive(`subNotebooks(id)`, (eb) =>
eb
.selectFrom((eb) =>
.selectFrom(() =>
sql<{ id: string }>`(VALUES ${sql.join(
ids.map((id) => eb.parens(sql`${id}`))
)})`.as("notebookIds")
ids.map((id) => sql.raw(`('${id}')`))
)})`.as("roots")
)
.selectAll()
.unionAll((eb) =>

View File

@@ -39,33 +39,47 @@ export default class Trash {
notebooks: [],
notes: []
};
private userDeletedCache: {
notes: string[];
notebooks: string[];
} = {
notebooks: [],
notes: []
};
constructor(private readonly db: Database) {}
async init() {
await this.cleanup();
await this.buildCache();
await this.cleanup();
}
async buildCache() {
this.cache.notes = [];
this.cache.notebooks = [];
this.userDeletedCache.notes = [];
this.userDeletedCache.notebooks = [];
const result = await this.db
.sql()
.selectFrom("notes")
.where("type", "==", "trash")
.select(["id", sql`'note'`.as("itemType")])
.select(["id", sql`'note'`.as("itemType"), "deletedBy"])
.unionAll((eb) =>
eb
.selectFrom("notebooks")
.where("type", "==", "trash")
.select(["id", sql`'notebook'`.as("itemType")])
.select(["id", sql`'notebook'`.as("itemType"), "deletedBy"])
)
.execute();
for (const { id, itemType } of result) {
if (itemType === "note") this.cache.notes.push(id);
else if (itemType === "notebook") this.cache.notebooks.push(id);
for (const { id, itemType, deletedBy } of result) {
if (itemType === "note") {
this.cache.notes.push(id);
if (deletedBy === "user") this.userDeletedCache.notes.push(id);
} else if (itemType === "notebook") {
this.cache.notebooks.push(id);
if (deletedBy === "user") this.userDeletedCache.notebooks.push(id);
}
}
}
@@ -116,6 +130,7 @@ export default class Trash {
deletedBy
});
this.cache.notes.push(...ids);
if (deletedBy === "user") this.userDeletedCache.notes.push(...ids);
} else if (type === "notebook") {
await this.db.notebooks.collection.update(ids, {
type: "trash",
@@ -124,24 +139,15 @@ export default class Trash {
deletedBy
});
this.cache.notebooks.push(...ids);
if (deletedBy === "user") this.userDeletedCache.notebooks.push(...ids);
}
}
async delete(...ids: string[]) {
if (ids.length <= 0) return;
const noteIds = [];
const notebookIds = [];
for (const id of ids) {
const isNote = this.cache.notes.includes(id);
if (isNote) {
noteIds.push(id);
this.cache.notes.splice(this.cache.notes.indexOf(id), 1);
} else if (!isNote) {
notebookIds.push(id);
this.cache.notebooks.splice(this.cache.notebooks.indexOf(id), 1);
}
}
const noteIds = ids.filter((id) => this.cache.notes.includes(id));
const notebookIds = ids.filter((id) => this.cache.notebooks.includes(id));
await this._delete(noteIds, notebookIds);
}
@@ -153,6 +159,7 @@ export default class Trash {
await this.db.noteHistory.clearSessions(...chunk);
await this.db.notes.remove(...chunk);
deleteItems(this.cache.notes, ...chunk);
deleteItems(this.userDeletedCache.notes, ...chunk);
}
}
@@ -162,6 +169,7 @@ export default class Trash {
await this.db.notebooks.remove(...chunk);
await this.db.relations.unlinkOfType("notebook", chunk);
deleteItems(this.cache.notebooks, ...chunk);
deleteItems(this.userDeletedCache.notebooks, ...chunk);
}
}
}
@@ -169,18 +177,8 @@ export default class Trash {
async restore(...ids: string[]) {
if (ids.length <= 0) return;
const noteIds = [];
const notebookIds = [];
for (const id of ids) {
const isNote = this.cache.notes.includes(id);
if (isNote) {
noteIds.push(id);
// this.cache.notes.splice(this.cache.notes.indexOf(id), 1);
} else if (!isNote) {
notebookIds.push(id);
// this.cache.notebooks.splice(this.cache.notebooks.indexOf(id), 1);
}
}
const noteIds = ids.filter((id) => this.cache.notes.includes(id));
const notebookIds = ids.filter((id) => this.cache.notebooks.includes(id));
if (noteIds.length > 0) {
await this.db.notes.collection.update(noteIds, {
@@ -190,6 +188,7 @@ export default class Trash {
deletedBy: null
});
deleteItems(this.cache.notes, ...noteIds);
deleteItems(this.userDeletedCache.notes, ...noteIds);
}
if (notebookIds.length > 0) {
@@ -201,12 +200,14 @@ export default class Trash {
deletedBy: null
});
deleteItems(this.cache.notebooks, ...ids);
deleteItems(this.userDeletedCache.notebooks, ...ids);
}
}
async clear() {
await this._delete(this.cache.notes, this.cache.notebooks);
this.cache = { notebooks: [], notes: [] };
this.userDeletedCache = { notebooks: [], notes: [] };
}
// synced(id: string) {
@@ -228,6 +229,7 @@ export default class Trash {
ids: string[],
deletedBy?: TrashItem["deletedBy"]
) {
if (ids.length <= 0) return [];
return (await this.db
.sql()
.selectFrom("notes")
@@ -242,6 +244,7 @@ export default class Trash {
ids: string[],
deletedBy?: TrashItem["deletedBy"]
) {
if (ids.length <= 0) return [];
return (await this.db
.sql()
.selectFrom("notebooks")
@@ -253,36 +256,31 @@ export default class Trash {
}
async grouped(options: GroupOptions) {
const ids = [...this.cache.notes, ...this.cache.notebooks];
const ids = [
...this.userDeletedCache.notes,
...this.userDeletedCache.notebooks
];
const selector = getSortSelectors(options)[options.sortDirection];
return new VirtualizedGrouping<TrashItem>(
ids.length,
this.db.options.batchSize,
() => Promise.resolve(ids),
async (start, end) => {
const notesRange =
end < this.cache.notes.length
? [start, end]
: [start, this.cache.notes.length];
const notebooksRange =
start >= this.cache.notes.length
? [start, end]
: [0, Math.min(this.cache.notebooks.length, end)];
const slicedIds = ids.slice(start, end);
const noteIds = slicedIds.filter((id) =>
this.userDeletedCache.notes.includes(id)
);
const notebookIds = slicedIds.filter((id) =>
this.userDeletedCache.notebooks.includes(id)
);
const items = [
...(await this.trashedNotes(
this.cache.notes.slice(notesRange[0], notesRange[1]),
"user"
)),
...(await this.trashedNotebooks(
this.cache.notebooks.slice(notebooksRange[0], notebooksRange[1]),
"user"
))
...(await this.trashedNotes(noteIds)),
...(await this.trashedNotebooks(notebookIds))
];
items.sort(selector);
return {
ids: ids.slice(start, end),
ids: slicedIds,
items
};
},
@@ -318,32 +316,19 @@ export default class Trash {
.selectAll()
.unionAll((eb) =>
eb
.selectFrom(["relations", "subNotebooks", "notebooks"])
.selectFrom(["relations", "subNotebooks"])
.select("relations.toId as id")
.where("toType", "==", "notebook")
.where("fromType", "==", "notebook")
.whereRef("fromId", "==", "subNotebooks.id")
.where(
(eb) =>
eb
.selectFrom("notebooks")
.whereRef("notebooks.id", "==", "relations.toId")
.where("notebooks.type", "==", "trash")
.limit(1)
.select("deletedBy"),
"!=",
"user"
)
.where("toId", "not in", this.userDeletedCache.notebooks)
.$narrowType<{ id: string }>()
)
)
.selectFrom("subNotebooks")
.select("id")
.where("id", "not in", notebookIds)
.execute();
return deleteItems(
ids.map((ref) => ref.id),
...notebookIds
);
return ids.map((ref) => ref.id);
}
}

View File

@@ -165,33 +165,69 @@ function Header({
flexDirection: "row"
}}
>
{!settings.premium && (
<Button
onPress={() => {
post(EventTypes.pro);
}}
preventDefault={false}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<CrownIcon
size={25 * settings.fontScale}
style={{
position: "absolute"
{tab.locked ? null : (
<>
<Button
onPress={() => {
editor?.commands.undo();
}}
color="orange"
/>
</Button>
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
if (tab.locked) return;
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
</>
)}
{settings.deviceMode !== "mobile" && !settings.fullscreen ? (
@@ -304,7 +340,11 @@ function Header({
<Button
fwdRef={btnRef}
onPress={() => {
setOpen(!isOpen);
if (tab.locked) {
post(EventTypes.properties, undefined, tab.id, tab.noteId);
} else {
setOpen(!isOpen);
}
}}
preventDefault={false}
style={{
@@ -320,13 +360,23 @@ function Header({
position: "relative"
}}
>
<DotsVerticalIcon
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_icon)"
/>
{tab.locked ? (
<DotsHorizontalIcon
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_icon)"
/>
) : (
<DotsVerticalIcon
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_icon)"
/>
)}
</Button>
<ControlledMenu
@@ -351,6 +401,9 @@ function Header({
tab.noteId
);
break;
case "search":
editor?.commands.startSearch();
break;
case "properties":
logger("info", "post properties...");
post(EventTypes.properties, undefined, tab.id, tab.noteId);
@@ -360,127 +413,66 @@ function Header({
}
}}
>
<div
<MenuItem
value="search"
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
width: "100%"
gap: 10,
alignItems: "center"
}}
>
<Button
onPress={() => {
editor?.commands.undo();
}}
<MagnifyIcon
size={22 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
<span
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
color: "var(--nn_primary_paragraph)"
}}
>
<ArrowULeftTopIcon
color={
!hasUndo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.redo();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<ArrowURightTopIcon
color={
!hasRedo
? "var(--nn_secondary_border)"
: "var(--nn_primary_icon)"
}
size={25 * settings.fontScale}
style={{
position: "absolute"
}}
/>
</Button>
<Button
onPress={() => {
editor?.commands.startSearch();
}}
style={{
borderWidth: 0,
borderRadius: 100,
color: "var(--nn_primary_icon)",
marginRight: 10,
width: 39,
height: 39,
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
<MagnifyIcon
size={28 * settings.fontScale}
style={{
position: "absolute"
}}
color="var(--nn_primary_icon)"
/>
</Button>
</div>
Search
</span>
</MenuItem>
<MenuItem
value="toc"
style={{
display: "flex",
gap: 10
gap: 10,
alignItems: "center"
}}
>
<TableOfContentsIcon
size={22 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
Table of contents
<span
style={{
color: "var(--nn_primary_paragraph)"
}}
>
Table of contents
</span>
</MenuItem>
<MenuItem
value="properties"
style={{
display: "flex",
gap: 10
gap: 10,
alignItems: "center"
}}
>
<DotsHorizontalIcon
size={22 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
Note Properties
<span
style={{
color: "var(--nn_primary_paragraph)"
}}
>
Properties
</span>
</MenuItem>
</ControlledMenu>
</div>

View File

@@ -61,10 +61,12 @@ export function ImageComponent(
threshold: 0.2,
once: true
});
const dom = editor.view.dom.parentElement || editor.view.dom;
const size =
editor.view.dom.clientWidth === 0
? node.attrs
: clampSize(node.attrs, editor.view.dom.clientWidth, aspectRatio);
: clampSize(node.attrs, dom.clientWidth, aspectRatio);
const float = isMobile ? false : node.attrs.float;