Compare commits

..

15 Commits

Author SHA1 Message Date
Ammar Ahmed
c6d999cf40 mobile: fix cannot read property 'sortDirection' of undefined 2024-05-01 12:26:06 +05:00
Abdullah Atta
b8c68b5163 web: fix middle pane size increasing on exiting focus mode 2024-04-30 21:52:25 +05:00
Abdullah Atta
2496004896 web: fix position of image preview toolbar 2024-04-30 21:51:57 +05:00
Abdullah Atta
16ec35de25 web: do not change properties top on web app 2024-04-30 21:51:57 +05:00
Abdullah Atta
d248b8e2b8 web: properties & toc side bars must be below the titlebar 2024-04-30 21:51:57 +05:00
Abdullah Atta
641e7484e3 desktop: add custom dns toggle in settings 2024-04-30 21:51:35 +05:00
Abdullah Atta
a46409ff44 core: add more fts trigger tests 2024-04-30 21:51:17 +05:00
Abdullah Atta
5dbabc2706 core: fix database disk image is malformed error on updating deleted content 2024-04-30 21:51:17 +05:00
Abdullah Atta
2d4d656dd9 web: canceling backup on password change should abort 2024-04-30 21:50:48 +05:00
Abdullah Atta
ab51b22e4a web: do not ask for password when downloading backup during recovery 2024-04-30 21:50:32 +05:00
Abdullah Atta
781d09daca web: fix navigation menu resize issues 2024-04-30 21:50:21 +05:00
Abdullah Atta
f21533e5e4 core: fix sql collection only getting first batch on iteration 2024-04-30 21:50:00 +05:00
Abdullah Atta
2ea9b04863 web: fix toggles in properties not working 2024-04-30 21:49:37 +05:00
Abdullah Atta
675d517cbc web: reduce zoom factor step to 0.1 2024-04-30 21:49:13 +05:00
Abdullah Atta
c336c57bda web: disable pwa 2024-04-30 21:48:28 +05:00
25 changed files with 334 additions and 47 deletions

View File

@@ -32,6 +32,7 @@ import { AssetManager } from "../utils/asset-manager";
import { isFlatpak } from "../utils";
import { setupDesktopIntegration } from "../utils/desktop-integration";
import { rm } from "fs/promises";
import { disableCustomDns, enableCustomDns } from "../utils/custom-dns";
const t = initTRPC.create();
@@ -55,6 +56,15 @@ export const osIntegrationRouter = t.router({
config.zoomFactor = factor;
}),
customDns: t.procedure.query(() => config.customDns),
setCustomDns: t.procedure
.input(z.boolean())
.mutation(({ input: customDns }) => {
if (customDns) enableCustomDns();
else disableCustomDns();
config.customDns = customDns;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())

View File

@@ -35,6 +35,7 @@ import path from "path";
import { bringToFront } from "./utils/bring-to-front";
import { bridge } from "./api/bridge";
import { setupDesktopIntegration } from "./utils/desktop-integration";
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
// only run a single instance
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
@@ -151,14 +152,8 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true,
secureDnsMode: "automatic"
});
if (config.customDns) enableCustomDns();
else disableCustomDns();
if (!isDevelopment()) registerProtocol();
await createWindow();

View File

@@ -43,6 +43,7 @@ export const config = {
theme: nativeTheme.themeSource,
automaticUpdates: true,
proxyRules: "",
customDns: true,
backgroundColor: nativeTheme.themeSource === "dark" ? "#0f0f0f" : "#ffffff",
windowControlsIconColor:

View File

@@ -0,0 +1,37 @@
/*
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 { app } from "electron";
export function enableCustomDns() {
app.configureHostResolver({
secureDnsServers: [
"https://mozilla.cloudflare-dns.com/dns-query",
"https://dns.quad9.net/dns-query"
],
enableBuiltInResolver: true
});
}
export function disableCustomDns() {
app.configureHostResolver({
secureDnsServers: [],
enableBuiltInResolver: true
});
}

View File

@@ -18,12 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useRef, useState } from "react";
import {
Platform,
TextInput,
View,
ScrollView as RNScrollView
} from "react-native";
import { Platform, TextInput, View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
@@ -369,7 +364,7 @@ export default function ReminderSheet({
))}
</View>
<RNScrollView showsHorizontalScrollIndicator={false} horizontal>
<ScrollView showsHorizontalScrollIndicator={false} horizontal>
{recurringMode === RecurringModes.Daily ||
recurringMode === RecurringModes.Year
? null
@@ -430,7 +425,7 @@ export default function ReminderSheet({
}}
/>
))}
</RNScrollView>
</ScrollView>
</View>
) : null}

View File

@@ -50,7 +50,7 @@ const Sort = ({ type, screen }) => {
const setOrderBy = async () => {
let _groupOptions = {
...groupOptions,
sortDirection: groupOptions.sortDirection === "asc" ? "desc" : "asc"
sortDirection: groupOptions?.sortDirection === "asc" ? "desc" : "asc"
};
if (type === "topics") {
_groupOptions.groupBy = "none";
@@ -85,7 +85,7 @@ const Sort = ({ type, screen }) => {
<Button
title={
groupOptions.sortDirection === "asc"
groupOptions?.sortDirection === "asc"
? groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
? "A - Z"
@@ -100,7 +100,7 @@ const Sort = ({ type, screen }) => {
: "New - Old"
}
icon={
groupOptions.sortDirection === "asc"
groupOptions?.sortDirection === "asc"
? "sort-ascending"
: "sort-descending"
}
@@ -229,7 +229,7 @@ const Sort = ({ type, screen }) => {
if (item === "abc") {
_groupOptions.sortBy = "title";
_groupOptions.sortDirection = "asc";
_groupOptions.sortDirection = "desc";
}
updateGroupOptions(_groupOptions);

View File

@@ -142,8 +142,13 @@ function DesktopAppContents({
}, [show]);
useEffect(() => {
if (isFocusMode) navPane.current?.collapse();
else navPane.current?.expand();
if (isFocusMode) {
const middlePaneSize = middlePane.current?.getSize() || 20;
navPane.current?.collapse();
// the middle pane has to be resized because collapsing the nav
// pane increases the middle pane's size every time.
middlePane.current?.resize(middlePaneSize);
} else navPane.current?.expand();
}, [isFocusMode]);
return (
@@ -159,9 +164,10 @@ function DesktopAppContents({
ref={navPane}
className="nav-pane"
defaultSize={10}
minSize={3}
onResize={(size) => setIsNarrow(size <= 3)}
minSize={3.5}
onResize={(size) => setIsNarrow(size <= 5)}
collapsible
collapsedSize={3.5}
>
<NavigationMenu
toggleNavigationContainer={(state) => {
@@ -192,7 +198,7 @@ function DesktopAppContents({
</ScopedThemeProvider>
</Panel>
<PanelResizeHandle className="panel-resize-handle" />
<Panel className="editor-pane">
<Panel className="editor-pane" defaultSize={70}>
<Flex
sx={{
display: "flex",

View File

@@ -77,18 +77,18 @@ export async function introduceFeatures() {
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
export async function createBackup() {
export async function createBackup(rescueMode = false) {
const { isLoggedIn } = useUserStore.getState();
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
const verified = encryptBackups || (await verifyAccount());
const verified = rescueMode || encryptBackups || (await verifyAccount());
if (!verified) {
showToast("error", "Could not create a backup: user verification failed.");
return;
return false;
}
const encryptedBackups = isLoggedIn && encryptBackups;
const encryptedBackups = !rescueMode && isLoggedIn && encryptBackups;
const filename = sanitizeFilename(
`${formatDate(Date.now(), {
@@ -139,7 +139,9 @@ export async function createBackup() {
console.error(error);
} else {
showToast("success", `Backup saved at ${filePath}.`);
return true;
}
return false;
}
export async function selectBackupFile() {

View File

@@ -46,6 +46,7 @@ import { Section } from "../properties";
import { scrollIntoViewById } from "@notesnook/editor";
import { Button, Flex, Text } from "@theme-ui/components";
import { useEditorManager } from "./manager";
import { TITLE_BAR_HEIGHT } from "../title-bar";
type TableOfContentsProps = {
sessionId: string;
@@ -100,7 +101,7 @@ function TableOfContents(props: TableOfContentsProps) {
display: "flex",
position: "absolute",
right: 0,
top: 0,
top: TITLE_BAR_HEIGHT,
zIndex: 999,
height: "100%",
width: "300px",

View File

@@ -29,6 +29,8 @@ import {
ZoomIn,
ZoomOut
} from "../icons";
import { getPlatform } from "../../utils/platform";
import { TITLE_BAR_HEIGHT } from "../title-bar";
const DEFAULT_ZOOM_STEP = 0.3;
const DEFAULT_LARGE_ZOOM = 4;
@@ -337,7 +339,12 @@ export class Lightbox extends React.Component<LightboxProps> {
borderRadius: "0px 0px 0px 5px",
overflow: "hidden",
alignItems: "center",
justifyContent: "flex-end"
justifyContent: "flex-end",
height: IS_DESKTOP_APP ? TITLE_BAR_HEIGHT : "auto",
pr:
IS_DESKTOP_APP && getPlatform() !== "darwin"
? "calc(100vw - env(titlebar-area-width))"
: 0
}}
>
{tools.map((tool) => (
@@ -349,6 +356,7 @@ export class Lightbox extends React.Component<LightboxProps> {
bg="transparent"
title={tool.title}
sx={{
height: "100%",
borderRadius: 0,
display: [
tool.hideOnMobile ? "none" : "flex",

View File

@@ -106,7 +106,7 @@ function NavigationItem(
<Button
data-test-id={`navigation-item`}
sx={{
px: 2,
px: isTablet ? 1 : 2,
flex: 1,
alignItems: "center",
justifyContent: isTablet ? "center" : "flex-start",
@@ -124,7 +124,10 @@ function NavigationItem(
}}
>
{image ? (
<Image src={image} sx={{ borderRadius: 50, size: 20 }} />
<Image
src={image}
sx={{ borderRadius: 50, size: 20, minWidth: 20, flexShrink: 0 }}
/>
) : Icon ? (
<Icon
size={isTablet ? 16 : 15}

View File

@@ -61,6 +61,7 @@ import {
} from "@notesnook/core";
import { VirtualizedTable } from "../virtualized-table";
import { TextSlice } from "@notesnook/core/dist/utils/content-block";
import { TITLE_BAR_HEIGHT } from "../title-bar";
const tools = [
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
@@ -129,7 +130,7 @@ function EditorProperties(props: EditorPropertiesProps) {
sx={{
display: "flex",
position: "absolute",
top: 0,
top: TITLE_BAR_HEIGHT,
right: 0,
zIndex: 999,
height: "100%",

View File

@@ -56,6 +56,7 @@ function Toggle(props: ToggleProps) {
<Switch
sx={{ m: 0, bg: isOn ? "accent" : "icon-secondary" }}
checked={isOn}
onClick={(e) => e.stopPropagation()}
/>
</Flex>
);

View File

@@ -29,6 +29,7 @@ import {
} from "../icons";
import { BaseThemeProvider } from "../theme-provider";
export const TITLE_BAR_HEIGHT = IS_DESKTOP_APP ? 37.8 : 0;
export function TitleBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
@@ -65,7 +66,7 @@ export function TitleBar() {
scope="titleBar"
sx={{
background: "background",
height: 37.8,
height: TITLE_BAR_HEIGHT,
display: "flex",
borderBottom: "1px solid var(--border)",
...(!isFullscreen && hasNativeWindowControls

View File

@@ -44,7 +44,8 @@ export const AppearanceSettings: SettingsGroup[] = [
type: "input",
inputType: "number",
min: 0.5,
max: 2.0,
max: 3.0,
step: 0.1,
defaultValue: () => useSettingStore.getState().zoomFactor,
onChange: (value) => useSettingStore.getState().setZoomFactor(value)
}

View File

@@ -46,7 +46,7 @@ export const AuthenticationSettings: SettingsGroup[] = [
title: "Change password",
variant: "secondary",
action: async () => {
await createBackup();
if (!(await createBackup())) return;
const result = await showPasswordDialog({
title: "Change account password",
message: `All your data will be re-encrypted and synced with the new password.

View File

@@ -508,6 +508,7 @@ function SettingItem(props: { item: Setting }) {
type={"number"}
min={component.min}
max={component.max}
step={component.step}
defaultValue={component.defaultValue()}
sx={{ width: 80, mr: 1 }}
onChange={debounce((e) => {

View File

@@ -111,6 +111,26 @@ What data is collected & when?`,
section: "privacy",
header: "Advanced",
settings: [
{
key: "custom-dns",
title: "Use custom DNS",
description: `Notesnook uses the following DNS providers:
1. Cloudflare DNS
2. Quad9
This can sometimes bypass local ISP blockages on Notesnook traffic. Disable this if you want the app to use system's DNS settings.`,
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.customDns, listener),
isHidden: () => !IS_DESKTOP_APP,
components: [
{
type: "toggle",
isToggled: () => useSettingStore.getState().customDns,
toggle: () => useSettingStore.getState().toggleCustomDns()
}
]
},
{
key: "custom-cors",
title: "Custom CORS proxy",

View File

@@ -134,6 +134,7 @@ export type NumberInputSettingComponent = BaseSettingComponent<"input"> & {
inputType: "number";
min: number;
max: number;
step?: number;
defaultValue: () => number;
onChange: (value: number) => void;
};

View File

@@ -40,7 +40,7 @@ async function renderApp() {
const { useKeyStore } = await import("./interfaces/key-store");
await useKeyStore.getState().init();
if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
// if (serviceWorkerWhitelist.includes(path)) await initializeServiceWorker();
const { default: Component } = await component();
const { default: AppLock } = await import("./views/app-lock");

View File

@@ -42,6 +42,7 @@ class SettingStore extends BaseStore<SettingStore> {
zoomFactor = 1.0;
privacyMode = false;
customDns = true;
hideNoteTitle = Config.get("hideNoteTitle", false);
telemetry = isTelemetryEnabled();
dateFormat = "DD-MM-YYYY";
@@ -67,6 +68,7 @@ class SettingStore extends BaseStore<SettingStore> {
desktopIntegrationSettings:
await desktop?.integration.desktopIntegration.query(),
privacyMode: await desktop?.integration.privacyMode.query(),
customDns: await desktop?.integration.customDns.query(),
zoomFactor: await desktop?.integration.zoomFactor.query(),
autoUpdates: await desktop?.updater.autoUpdates.query(),
proxyRules: await desktop?.integration.proxyRules.query()
@@ -177,6 +179,12 @@ class SettingStore extends BaseStore<SettingStore> {
await desktop?.integration.setPrivacyMode.mutate({ enabled: !privacyMode });
};
toggleCustomDns = async () => {
const customDns = this.get().customDns;
this.set({ customDns: !customDns });
await desktop?.integration.setCustomDns.mutate(!customDns);
};
toggleHideTitle = async () => {
const { hideNoteTitle } = this.get();
this.set({ hideNoteTitle: !hideNoteTitle });

View File

@@ -460,7 +460,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
"Please wait while we create a backup file for you to download."
}}
onSubmit={async () => {
await createBackup();
await createBackup(true);
navigate("new");
}}
>

View File

@@ -0,0 +1,176 @@
/*
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 { expect, test } from "vitest";
import { TEST_NOTE, databaseTest, noteTest } from "./utils";
test("updating deleted content should not throw", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
const contentId = await db.content.add({
data: "helloworld",
noteId: id
});
await db.content.remove(contentId!);
await expect(
db.content.collection.update(
[contentId!],
{ synced: true },
{ sendEvent: false }
)
).resolves.toBeFalsy();
}));
test("updating content should not break full text search", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
const contentId = await db.content.add({
data: "hello world",
noteId: id
});
await db.content.add({
id: contentId,
data: "i am amazing",
noteId: id
});
expect(await db.lookup.notes("amazing").ids()).toContain(id);
expect(await db.lookup.notes("hello world").ids()).not.toContain(id);
}));
test("updating note title should not break full text search", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
await db.notes.add({
id,
title: "What an amazing note!"
});
expect(await db.lookup.notes("amazing").ids()).toContain(id);
expect(await db.lookup.notes("new note").ids()).not.toContain(id);
}));
test("updating deleted note should not throw", () =>
databaseTest().then(async (db) => {
const id = await db.notes.add({
title: "New note"
});
await db.notes.remove(id);
await expect(
db.notes.collection.update([id], { synced: true }, { sendEvent: false })
).resolves.toBeFalsy();
}));
test("overwriting unlocked content with locked content should update search index", () =>
noteTest({
content: {
data: "hello world",
type: "tiptap"
}
}).then(async ({ db, id }) => {
await db.content.collection.upsert({
id: "something",
locked: false,
data: "What is this?",
noteId: id,
dateCreated: Date.now(),
dateEdited: Date.now(),
synced: false,
dateModified: Date.now()
});
await db.content.collection.put([
{
id: "something",
locked: true,
data: {
alg: "as",
cipher: "s",
format: "base64",
iv: "",
length: 20,
salt: ""
},
noteId: id,
dateCreated: Date.now(),
dateEdited: Date.now(),
synced: false,
dateModified: Date.now()
}
]);
expect(await db.lookup.notes("what is this").ids()).not.toContain(id);
}));
test("overwriting content with deleted content should update search index", () =>
noteTest({
content: {
data: "hello world",
type: "tiptap"
}
}).then(async ({ db, id }) => {
await db.content.collection.upsert({
id: "something",
locked: false,
data: "What is this?",
noteId: id,
dateCreated: Date.now(),
dateEdited: Date.now(),
synced: false,
dateModified: Date.now()
});
await db.content.collection.put([
{
id: "something",
deleted: true,
synced: true,
dateModified: Date.now()
}
]);
expect(await db.lookup.notes("what is this").ids()).not.toContain(id);
}));
test("overwriting note with deleted note should update search index", () =>
noteTest({
title: "I am title"
}).then(async ({ db, id }) => {
await db.notes.collection.put([
{
id,
deleted: true,
synced: true,
dateModified: Date.now()
}
]);
expect(await db.lookup.notes("title").ids()).not.toContain(id);
}));

View File

@@ -44,7 +44,6 @@ import {
Kysely,
SelectQueryBuilder,
SqlBool,
Transaction,
sql
} from "kysely";
import { VirtualizedGrouping } from "../utils/virtualized-grouping";
@@ -338,8 +337,10 @@ export class SQLCollection<
}
export class FilteredSelector<T extends Item> {
private _fields: AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>[] =
[];
private _fields: (
| AnyColumn<DatabaseSchema, keyof DatabaseSchema>
| AnyColumnWithTable<DatabaseSchema, keyof DatabaseSchema>
)[] = [];
filter: SelectQueryBuilder<DatabaseSchema, keyof DatabaseSchema, unknown>;
private _limit = 0;
constructor(
@@ -524,6 +525,11 @@ export class FilteredSelector<T extends Item> {
async *[Symbol.asyncIterator]() {
let lastRow: any | null = null;
const fields = this._fields.slice();
if (!fields.find((f) => f.includes(".dateCreated")))
fields.push("dateCreated");
if (!fields.find((f) => f.includes(".id"))) fields.push("id");
while (true) {
const rows = await this.filter
.orderBy("dateCreated asc")
@@ -536,8 +542,8 @@ export class FilteredSelector<T extends Item> {
)
)
.limit(this.batchSize)
.$if(this._fields.length === 0, (eb) => eb.selectAll())
.$if(this._fields.length > 0, (eb) => eb.select(this._fields))
.$if(fields.length === 0, (eb) => eb.selectAll())
.$if(fields.length > 0, (eb) => eb.select(fields))
.execute();
if (rows.length === 0) break;
for (const row of rows) {

View File

@@ -31,8 +31,8 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.addEvent("insert")
.when((eb) =>
eb.and([
eb.or([eb("new.deleted", "is", null), eb("new.deleted", "==", false)]),
eb.or([eb("new.locked", "is", null), eb("new.locked", "==", false)]),
eb("new.deleted", "is not", true),
eb("new.locked", "is not", true),
eb("new.data", "is not", null)
])
)
@@ -71,6 +71,13 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.onTable("content", "main")
.after()
.addEvent("update")
.when((eb) =>
eb.and([
eb("old.deleted", "is not", true),
eb("old.noteId", "is not", null),
eb("old.data", "is not", null)
])
)
.addQuery((c) =>
c.insertInto("content_fts").values({
content_fts: sql.lit("delete"),
@@ -135,7 +142,13 @@ export async function createTriggers(db: Kysely<RawDatabaseSchema>) {
.ifNotExists()
.onTable("notes", "main")
.after()
.addEvent("update", ["title"])
.addEvent("update")
.when((eb) =>
eb.and([
eb("old.deleted", "is not", true),
eb("old.title", "is not", null)
])
)
.addQuery((c) =>
c.insertInto("notes_fts").values({
notes_fts: sql.lit("delete"),