Compare commits

...

69 Commits

Author SHA1 Message Date
Licaon_Kter
c54759e766 Remove gradle property unused in Java 17
Signed-off-by: Licaon_Kter <licaon-kter@users.noreply.github.com>
2024-03-22 23:17:45 +05:00
Ammar Ahmed
f15446b1d6 mobile: bump version code 2024-03-21 11:45:03 +05:00
Ammar Ahmed
aa2a8ab6f4 mobile: ignore beta updates on gh release 2024-03-21 10:17:34 +05:00
Ammar Ahmed
8e59ff1318 mobile: release v2.6.18 2024-03-21 10:11:37 +05:00
Abdullah Atta
ad99c87f72 web: bump version to 2.6.17 2024-03-21 10:07:11 +05:00
Abdullah Atta
ba776b5854 global: update lockfiles 2024-03-21 10:07:02 +05:00
Abdullah Atta
e526145e48 web: fix cannot read property integration of undefined 2024-03-21 10:02:08 +05:00
Abdullah Atta
45c5e65bf6 core: add more sanity checks 2024-03-21 09:42:38 +05:00
Abdullah Atta
87bec4e2ab core: add sanity checks to handle cannot read properties of undefined (reading 'hash') error 2024-03-21 09:37:18 +05:00
Abdullah Atta
433e67a6dc web: fix pull failed error on downloading attachments 2024-03-21 09:12:55 +05:00
Ammar Ahmed
6acba51b88 mobile: fix app lock shows on session expired dialog 2024-03-21 07:46:49 +05:00
Abdullah Atta
cf1322952d web: handle errors during attachment downloading 2024-03-15 09:25:10 +05:00
Abdullah Atta
b5ac9dd430 web: make opfs safer by allowing only 1 operation at a time 2024-03-15 09:01:35 +05:00
Abdullah Atta
a3b590bcb7 core: add more login tests 2024-03-12 07:19:59 +05:00
Ammar Ahmed
d4f7e3ef07 mobile: add changelog 2024-03-06 15:31:33 +05:00
Abdullah Atta
16119f6958 setup: optimize bootstrap script 2024-03-06 13:40:42 +05:00
Abdullah Atta
e39c2583a5 setup: only run npm rebuild if required 2024-03-06 13:40:41 +05:00
Abdullah Atta
eecff009dd setup: run npm rebuild after bootstrap 2024-03-06 13:40:41 +05:00
Abdullah Atta
d9ed61a7da setup: ignore scripts during bootstrap 2024-03-06 13:40:41 +05:00
Abdullah Atta
58db265702 setup: use 1 thread on CI 2024-03-06 13:40:41 +05:00
Abdullah Atta
bd39f0cbf8 core: fix e.metadata.hash is not defined 2024-03-06 13:29:59 +05:00
Abdullah Atta
255015108a core: update lockfile 2024-03-06 13:01:00 +05:00
Abdullah Atta
b80f366f8d core: run patch-package after install 2024-03-06 13:00:34 +05:00
Ammar Ahmed
c845c7fce6 mobile: release v2.6.17 2024-03-06 12:59:23 +05:00
Ammar Ahmed
6e7b7ce0e1 mobile: ask user to save recovery key after changing password 2024-03-06 12:33:38 +05:00
Abdullah Atta
9b728902d4 web: bump version to 2.6.16 2024-03-06 12:03:06 +05:00
Abdullah Atta
9a1f54c41a desktop: set proxy on startup 2024-03-06 11:43:50 +05:00
Abdullah Atta
b3251eb016 core: fix login gets stuck on error 2024-03-06 11:28:45 +05:00
Abdullah Atta
f4064b7939 core: patch signalr to exclude electron renderer process as node 2024-03-06 11:28:27 +05:00
Abdullah Atta
f014f08ff4 web: fix export to html test snapshot 2024-03-05 15:41:53 +05:00
Abdullah Atta
dee1cf4b5a web: fix locked note status not updating in list 2024-03-05 15:38:01 +05:00
Abdullah Atta
f35a3d8fa3 web: clear editor on unlocking the note 2024-03-05 15:37:45 +05:00
Abdullah Atta
ca9a8d14fa core: fix sync tests 2024-03-05 15:03:18 +05:00
Abdullah Atta
35c35fa9d7 core: update test snapshots 2024-03-05 14:51:41 +05:00
Abdullah Atta
09fff95d58 editor: fix marks.filter is not a function error on pressing Enter 2024-03-05 14:27:29 +05:00
Abdullah Atta
287307a1ba core: move to json protocol for signalr 2024-03-05 14:23:44 +05:00
Abdullah Atta
d628e62302 web: fix app hangs when downloading logs 2024-03-05 14:18:54 +05:00
Ammar Ahmed
a070bc46ad mobile: check backup was successful 2024-03-05 14:16:35 +05:00
Ammar Ahmed
9027c44511 mobile: request backup before changing password 2024-03-05 14:16:35 +05:00
Abdullah Atta
722aa7b9c6 mobile: update database sync api to use new sync options 2024-03-05 14:16:35 +05:00
Abdullah Atta
a4e777b24b web: update database sync api to use new sync options 2024-03-05 14:16:35 +05:00
Abdullah Atta
af7120e953 web: create backup before changing password 2024-03-05 14:16:35 +05:00
Abdullah Atta
d5adc3bbac web: refactor backup verification logic 2024-03-05 14:16:35 +05:00
Abdullah Atta
aa3d778ad1 core: reduce unnecessary syncing during password change 2024-03-05 14:16:35 +05:00
Abdullah Atta
3465a816c6 web: rename to markdown shortcuts 2024-03-05 13:42:39 +05:00
Ammar Ahmed
3d1726c5aa mobile: toggle markdown shortcuts 2024-03-05 13:42:39 +05:00
Muhammad Ali
488de2d583 web: add setting for toggling markdown in editor 2024-03-05 13:42:39 +05:00
Abdullah Atta
c415b114a6 web: template not disabling when copying as markdown 2024-03-05 13:32:54 +05:00
Abdullah Atta
da36784e9c core: fix content inside span not converting to markdown 2024-03-05 13:32:54 +05:00
Muhammad Ali
78fafdc272 editor: convert google docs checklists on paste 2024-03-05 13:32:54 +05:00
Muhammad Ali
7d57055f73 editor: fix new lines from google docs not being preserved on paste (#3988)
* editor: handle <br> directly under root element

* editor: fix pasting from google docs

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-05 12:14:19 +05:00
Ammar Ahmed
b84cc747cb mobile: restore backup with encryption key 2024-03-04 15:56:12 +05:00
Abdullah Atta
9a4f3ccbad web: allow using encryption key to restore backup 2024-03-04 15:56:12 +05:00
Abdullah Atta
68de3cc928 core: allow using encryption key to decrypt backup 2024-03-04 15:56:12 +05:00
Ammar Ahmed
6e135cdd36 mobile: sort reminders 2024-03-04 15:27:42 +05:00
Abdullah Atta
696116ac85 web: add support for sorting reminders 2024-03-04 15:27:42 +05:00
Abdullah Atta
08a13ff35a core: add support for group options for reminders 2024-03-04 15:27:42 +05:00
Abdullah Atta
af1cea81dc theme: use radio focus color from theme 2024-03-04 15:27:42 +05:00
Muhammad Ali
2c22a7ca90 core: add support for sorting reminders by due date 2024-03-04 15:27:42 +05:00
Muhammad Ali
6fd6bd0375 desktop: add proxy settings (#4196) 2024-03-04 15:16:59 +05:00
Muhammad Ali
1a96dc965a desktop: add support for viewing and removing custom dictionary words (#4362)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-04 13:47:24 +05:00
Muhammad Ali
9550350f25 desktop: add menu item to copy images in editor (#4373) 2024-03-04 13:27:46 +05:00
Muhammad Ali
0ed605a5b9 editor: fix column toolbar position when table is scrolled horizontally (#4366) 2024-03-04 13:25:46 +05:00
Abdullah Atta
18910d931c config: add .nx to .gitignore 2024-03-04 13:24:25 +05:00
Muhammad Ali
87648b7d8b editor: fix katex math not being rendered correctly (#4353)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-04 13:23:27 +05:00
Muhammad Ali
7e7a1aeecb editor: add support for toggling highlight/background color (#4330) 2024-03-04 13:22:25 +05:00
Muhammad Ali
cb353358b7 web: fix editor text not selectable from margins (#4292)
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-03-04 13:20:40 +05:00
Muhammad Ali
ba1619711b desktop: fix app autostart in appimage (#4223) 2024-03-04 12:40:51 +05:00
Ammar Ahmed
b3cc9475ad mobile: fix attachment search 2024-03-01 08:22:14 +05:00
111 changed files with 1413 additions and 586 deletions

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ nx-cloud.env
.env.local
.nyc_output
site
node_modules.backup
node_modules.backup
.nx

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.6.15",
"version": "2.6.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.15",
"version": "2.6.17",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.15",
"version": "2.6.17",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",

View File

@@ -54,6 +54,14 @@ export const osIntegrationRouter = t.router({
config.zoomFactor = factor;
}),
proxyRules: t.procedure.query(() => config.proxyRules),
setProxyRules: t.procedure
.input(z.string().optional())
.mutation(({ input: proxyRules }) => {
globalThis.window?.webContents.session.setProxy({ proxyRules });
config.proxyRules = proxyRules || "";
}),
privacyMode: t.procedure.query(() => config.privacyMode),
setPrivacyMode: t.procedure
.input(z.object({ enabled: z.boolean() }))

View File

@@ -118,5 +118,13 @@ export const spellCheckerRouter = t.router({
.mutation(({ input: { enabled } }) => {
globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled);
config.isSpellCheckerEnabled = enabled;
})
}),
words: t.procedure.query(() =>
globalThis.window?.webContents.session.listWordsInSpellCheckerDictionary()
),
deleteWord: t.procedure.input(z.string()).mutation(({ input: word }) => {
globalThis.window?.webContents.session.removeWordFromSpellCheckerDictionary(
word
);
})
});

View File

@@ -112,6 +112,7 @@ async function createWindow() {
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
"http://dictionaries.notesnook.com/"
);
mainWindow.webContents.session.setProxy({ proxyRules: config.proxyRules });
mainWindow.once("closed", () => {
globalThis.window = null;

View File

@@ -25,7 +25,11 @@ Type=Application
Version=${app.getVersion()}
Name=${app.getName()}
Comment=${app.getName()} startup script
Exec=${process.execPath}${hidden ? " --hidden" : ""}
Exec=${
process.env.APPIMAGE
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
: `${process.execPath}${hidden ? " --hidden" : ""}`
}
StartupNotify=false
Terminal=false`;

View File

@@ -41,7 +41,8 @@ export const config = {
isSpellCheckerEnabled: true,
zoomFactor: 1,
theme: nativeTheme.themeSource,
automaticUpdates: true
automaticUpdates: true,
proxyRules: ""
};
type ConfigKey = keyof typeof config;

View File

@@ -131,6 +131,17 @@ function setupMenu() {
);
}
if (params.mediaType === "image")
menu.append(
new MenuItem({
id: "copy-image",
label: "Copy Image",
click() {
globalThis.window?.webContents.copyImageAt(params.x, params.y);
}
})
);
if (params.isEditable)
menu.append(
new MenuItem({

View File

@@ -49,7 +49,7 @@ export const AttachmentDialog = ({ note }) => {
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])]
);
@@ -59,8 +59,8 @@ export const AttachmentDialog = ({ note }) => {
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
const attachments = note?.id
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
attachmentSearchValue.current = text;
@@ -137,10 +137,11 @@ export const AttachmentDialog = ({ note }) => {
];
const filterAttachments = (type, _attachments) => {
const attachments =
_attachments || note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
const attachments = _attachments
? _attachments
: note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
switch (type) {
case "all":

View File

@@ -26,12 +26,15 @@ import {
ToastEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { Notice } from "../ui/notice";
import Seperator from "../ui/seperator";
import { Dialog } from "../dialog";
import BackupService from "../../services/backup";
import { sleep } from "../../utils/time";
export const ChangePassword = () => {
const passwordInputRef = useRef();
@@ -64,6 +67,9 @@ export const ChangePassword = () => {
}
setLoading(true);
try {
const result = await BackupService.run(false, "change-password-dialog");
if (!result) throw new Error("Failed to create backup");
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastEvent.show({
@@ -73,6 +79,8 @@ export const ChangePassword = () => {
});
setLoading(false);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
ToastEvent.show({
@@ -92,6 +100,7 @@ export const ChangePassword = () => {
padding: 12
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
@@ -129,7 +138,14 @@ export const ChangePassword = () => {
/>
<Notice
text="Changing password is a non-undoable process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
type="alert"
/>
<View style={{ height: 10 }} />
<Notice
text={`Once your password is changed, please make sure to save the new account recovery key.`}
type="alert"
/>

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { Modal, View } from "react-native";
import { View } from "react-native";
import { db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import BiometricService from "../../services/biometrics";
@@ -37,6 +37,7 @@ import { eLoginSessionExpired, eUserLoggedIn } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import { presentDialog } from "../dialog/functions";
import SheetProvider from "../sheet-provider";
import { Toast } from "../toast";
@@ -46,7 +47,6 @@ import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { LoginSteps, useLogin } from "./use-login";
import BaseDialog from "../dialog/base-dialog";
function getObfuscatedEmail(email) {
if (!email) return "";
@@ -69,6 +69,9 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
@@ -84,6 +87,9 @@ export const SessionExpired = () => {
MMKV.clearStore();
clearAllStores();
setVisible(false);
useUserStore.setState({
disableAppLockRequests: false
});
} catch (e) {
ToastEvent.show({
heading: e.message,
@@ -127,6 +133,9 @@ export const SessionExpired = () => {
email.current = user.email;
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
@@ -142,6 +151,9 @@ export const SessionExpired = () => {
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
useUserStore.setState({
disableAppLockRequests: true
});
}}
enableSheetKeyboardHandler={true}
visible={true}

View File

@@ -40,7 +40,10 @@ import { Button } from "../ui/button";
export const Dialog = ({ context = "global" }) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [inputValue, setInputValue] = useState(null);
const [checked, setChecked] = useState(false);
const values = useRef({
inputValue: undefined
});
const inputRef = useRef();
const [dialogInfo, setDialogInfo] = useState({
title: "",
@@ -76,7 +79,8 @@ export const Dialog = ({ context = "global" }) => {
if (dialogInfo.positivePress) {
inputRef.current?.blur();
let result = await dialogInfo.positivePress(
inputValue || dialogInfo.defaultValue
values.current.inputValue || dialogInfo.defaultValue,
checked
);
if (result === false) {
return;
@@ -91,14 +95,16 @@ export const Dialog = ({ context = "global" }) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
setDialogInfo(data);
setChecked(false);
values.current.inputValue = data.defaultValue;
setVisible(true);
setInputValue(data.defaultValue);
},
[context]
);
const hide = () => {
setInputValue(null);
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
};
@@ -106,7 +112,6 @@ export const Dialog = ({ context = "global" }) => {
if (dialogInfo.onClose) {
await dialogInfo.onClose();
}
hide();
};
@@ -159,7 +164,7 @@ export const Dialog = ({ context = "global" }) => {
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(value) => {
setInputValue(value);
values.current.inputValue = value;
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
@@ -176,10 +181,10 @@ export const Dialog = ({ context = "global" }) => {
<>
<Button
onPress={() => {
setInputValue(!inputValue);
setChecked(!checked);
}}
icon={
inputValue
checked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
@@ -187,9 +192,10 @@ export const Dialog = ({ context = "global" }) => {
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={inputValue ? dialogInfo.check.type : "gray"}
type={checked ? dialogInfo.check.type : "gray"}
/>
</>
) : null}

View File

@@ -101,7 +101,7 @@ export const SectionHeader = React.memo(
alignItems: "center"
}}
>
{index === 0 && (
{index === 0 ? (
<>
<Button
onPress={() => {
@@ -109,7 +109,6 @@ export const SectionHeader = React.memo(
component: <Sort screen={screen} type={type} />
});
}}
hidden={screen === "Reminders"}
tooltipText="Change sorting of items in list"
fwdRef={sortRef}
title={groupBy}
@@ -160,7 +159,7 @@ export const SectionHeader = React.memo(
size={SIZE.lg - 2}
/>
</>
)}
) : null}
</View>
</View>
);

View File

@@ -17,17 +17,19 @@ 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 { getFormattedDate } from "@notesnook/common";
import { EVENTS } from "@notesnook/core/dist/common";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import RNFetchBlob from "react-native-blob-util";
import DocumentPicker, {
DocumentPickerResponse
} from "react-native-document-picker";
import DocumentPicker from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";
import { db } from "../../../common/database";
import storage from "../../../common/database/storage";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
import {
ToastEvent,
eSubscribeEvent,
@@ -35,7 +37,6 @@ import {
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { initialize } from "../../../stores";
import { useThemeColors } from "@notesnook/theme";
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
@@ -46,9 +47,6 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { unzip } from "react-native-zip-archive";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);
@@ -184,7 +182,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
title: "Encrypted backup",
input: true,
inputPlaceholder: "Password",
paragraph: "Please enter password of this backup file to restore it",
paragraph: "Please enter password of this backup file",
positiveText: "Restore",
secureTextEntry: true,
onClose: () => {
@@ -192,10 +190,17 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
resolve(undefined);
},
negativeText: "Cancel",
positivePress: async (password) => {
resolve(password);
positivePress: async (password, isEncryptionKey) => {
resolve({
value: password,
isEncryptionKey
});
resolved = true;
return true;
},
check: {
info: "Use encryption key",
type: "transparent"
}
});
});
@@ -235,8 +240,8 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}
};
const restoreBackup = async (backup, password) => {
await db.backup.import(backup, password);
const restoreBackup = async (backup, password, key) => {
await db.backup.import(backup, password, key);
await db.initCollections();
initialize();
ToastEvent.show({
@@ -279,6 +284,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}
let password;
let key;
console.log(`Found ${backupFiles?.length} files to restore from backup`);
for (const path of backupFiles) {
@@ -289,10 +295,16 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
if (parsed.encrypted && !password) {
console.log("Backup is encrypted...", "requesting password");
password = await withPassword();
if (!password) throw new Error("Failed to decrypt backup");
const { value, isEncryptionKey } = await withPassword();
if (isEncryptionKey) {
key = value;
} else {
password = value;
}
if (!password && !key) throw new Error("Failed to decrypt backup");
}
await db.backup.import(parsed, password);
await db.backup.import(parsed, password, key);
console.log("Imported", path);
}
// Remove files from cache
@@ -323,10 +335,19 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
async function restoreFromNNBackup(backup) {
try {
if (backup.data.iv && backup.data.salt) {
const password = await withPassword();
if (password) {
const { value, isEncryptionKey } = await withPassword();
let key;
let password;
if (isEncryptionKey) {
key = value;
} else {
password = value;
}
if (key || password) {
try {
await restoreBackup(backup, password);
await restoreBackup(backup, password, key);
close();
setRestoring(false);
} catch (e) {

View File

@@ -88,10 +88,14 @@ const Sort = ({ type, screen }) => {
? groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
? "A - Z"
: groupOptions.sortBy === "dueDate"
? "Earliest first"
: "Old - New"
: groupOptions.groupBy === "abc" ||
groupOptions.sortBy === "title"
? "Z - A"
: groupOptions.sortBy === "dueDate"
? "Latest first"
: "New - Old"
}
icon={
@@ -142,6 +146,7 @@ const Sort = ({ type, screen }) => {
/>
) : (
Object.keys(SORT).map((item) =>
(item === "dueDate" && screen !== "Reminders") ||
(item === "title" && groupOptions.groupBy !== "none") ||
((screen !== "Tags" || screen !== "Reminders") &&
item === "dateModified") ||

View File

@@ -53,6 +53,7 @@ export type Settings = {
dateFormat: string;
timeFormat: string;
fontScale: number;
markdownShortcuts: boolean;
};
export type EditorProps = {

View File

@@ -157,6 +157,9 @@ export const useEditorEvents = (
const defaultFontFamily = useSettingStore(
(state) => state.settings.defaultFontFamily
);
const markdownShortcuts = useSettingStore(
(state) => state.settings.markdownShortcuts
);
const tools = useDragState((state) => state.data);
@@ -202,7 +205,8 @@ export const useEditorEvents = (
fontFamily: SettingsService.get().defaultFontFamily,
dateFormat: db.settings?.getDateFormat(),
timeFormat: db.settings?.getTimeFormat(),
fontScale
fontScale,
markdownShortcuts
});
}, [
fullscreen,
@@ -222,7 +226,8 @@ export const useEditorEvents = (
dateFormat,
timeFormat,
loading,
fontScale
fontScale,
markdownShortcuts
]);
const onBackPress = useCallback(async () => {

View File

@@ -584,6 +584,13 @@ export const settingsGroups: SettingSection[] = [
component: "title-format",
description: "Customize the formatting for new note title",
type: "component"
},
{
id: "toggle-markdown",
name: "Markdown shortcuts",
property: "markdownShortcuts",
description: "Toggle markdown in the editor",
type: "switch"
}
]
}

View File

@@ -213,10 +213,12 @@ async function run(progress, context) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
updateNextBackupTime();
let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
let showBackupCompleteSheet =
progress && SettingsService.get().showBackupCompleteSheet;
if (context) return path;
await sleep(300);
if (showBackupCompleteSheet) {
presentBackupCompleteSheet(backupFilePath);
} else {
@@ -233,7 +235,7 @@ async function run(progress, context) {
return path;
} catch (e) {
await sleep(300);
eSendEvent(eCloseSheet);
progress && eSendEvent(eCloseSheet);
ToastEvent.error(e, "Backup failed!");
return null;
}

View File

@@ -262,7 +262,7 @@ const onEvent = async ({ type, detail }: Event) => {
if (status.isInternetReachable) {
try {
if (!globalThis["IS_MAIN_APP_RUNNING" as never]) {
await db.sync(false, false);
await db.sync({ type: "send", force: false });
} else {
console.log("main app running, skipping sync");
}
@@ -904,7 +904,7 @@ async function pinNote(id: string) {
const note = db.notes?.note(id as string) as any;
let text = await convertNoteToText(note as any, false);
if (!text) text = "";
let html = text.replace(/\n/g, "<br />");
const html = text.replace(/\n/g, "<br />");
Notifications.displayNotification({
title: note.title,
message: note.headline || text,

View File

@@ -79,7 +79,11 @@ const run = async (
try {
await BackgroundSync.doInBackground(async () => {
try {
await db.sync(full, forced, lastSyncTime);
await db.sync({
type: full ? "full" : "send",
force: forced,
lastSyncTime
});
} catch (e) {
error = e;
}

View File

@@ -32,7 +32,10 @@ export const useReminderStore = create<ReminderStore>((set) => ({
reminders: [],
setReminders: () => {
set({
reminders: groupReminders((db.reminders?.all as Reminder[]) || [])
reminders: groupReminders(
(db.reminders?.all as Reminder[]) || [],
db.settings?.getGroupOptions("reminders")
)
});
},
cleareReminders: () => set({ reminders: [] })

View File

@@ -74,6 +74,7 @@ export type Settings = {
colorScheme: "dark" | "light";
lighTheme: ThemeDefinition;
darkTheme: ThemeDefinition;
markdownShortcuts?: boolean;
};
type DimensionsType = {
@@ -154,7 +155,8 @@ export const defaultSettings: SettingStore["settings"] = {
defaultFontSize: 16,
colorScheme: "light",
lighTheme: ThemeLight,
darkTheme: ThemeDark
darkTheme: ThemeDark,
markdownShortcuts: true
};
export const useSettingStore = create<SettingStore>((set, get) => ({

View File

@@ -41,7 +41,8 @@ export const SORT = {
dateModified: "Date modified",
dateEdited: "Date edited",
dateCreated: "Date created",
title: "Title"
title: "Title",
dueDate: "Due date"
};
export const itemSkus = [

View File

@@ -36,7 +36,7 @@ function confirmDeleteAllNotes(items, type, context) {
}?`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: (value) => {
positivePress: (_inputValue, value) => {
setTimeout(() => {
resolve({ delete: true, deleteNotes: value });
});

View File

@@ -61,7 +61,10 @@ export const getGithubVersion = async () => {
if (!res?.ok) return null;
const data = (await res?.json()) as GithubRelease[];
const versions = data?.filter((tag) => tag.tag_name.endsWith("android"));
const versions = data?.filter(
(tag) =>
tag.tag_name.endsWith("android") && !tag.tag_name.endsWith("beta-android")
);
const latestVersion = versions[0];
const version = latestVersion.tag_name.replace("-android", "");
return {

View File

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

View File

@@ -10,7 +10,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
@@ -47,4 +47,4 @@ hermesEnabled=true
# v8.android.tools.dir=/home/ammarahm-ed/Repos/notesnook-mobile/node_modules/v8-android-jit-nointl/dist/tools/android
# fdroid
fdroidBuild=false
fdroidBuild=false

View File

@@ -1,10 +1,3 @@
- Support for exporting locked notes
- Maintain directory structure in exports based on Notebooks/Topics
- Match color of navigation bar in sheets with app theme
- Improved image loading and show progress when loading images
- Support for setting trash interval to daily
- Fix android app UI issues with RTL languages
- Fix pasting links in editor
- Other Bug fixes and performance improvements
- Minor bug fixes
Thank you for using Notesnook!

View File

@@ -997,7 +997,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2067;
CURRENT_PROJECT_VERSION = 2090;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1071,7 +1071,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.15;
MARKETING_VERSION = 2.6.18;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1102,7 +1102,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2067;
CURRENT_PROJECT_VERSION = 2090;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1176,7 +1176,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.15;
MARKETING_VERSION = 2.6.18;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1335,7 +1335,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2067;
CURRENT_PROJECT_VERSION = 2090;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1347,7 +1347,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.15;
MARKETING_VERSION = 2.6.18;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1378,7 +1378,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2067;
CURRENT_PROJECT_VERSION = 2090;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1391,7 +1391,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.15;
MARKETING_VERSION = 2.6.18;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1421,7 +1421,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2067;
CURRENT_PROJECT_VERSION = 2090;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1495,7 +1495,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.15;
MARKETING_VERSION = 2.6.18;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1526,7 +1526,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2067;
CURRENT_PROJECT_VERSION = 2090;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1601,7 +1601,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.15;
MARKETING_VERSION = 2.6.18;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "2.6.15",
"version": "2.6.17",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "2.6.15",
"version": "2.6.17",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -57,13 +57,13 @@
"../../packages/core": {
"name": "@notesnook/core",
"version": "7.4.1",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -26837,12 +26837,11 @@
"@notesnook/core": {
"version": "file:../../packages/core",
"requires": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/crypto": "file:../crypto",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"@types/html-to-text": "^9.0.0",
"@types/katex": "^0.16.1",
"@types/prismjs": "^1.26.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "2.6.15",
"version": "2.6.18",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -45,4 +45,4 @@
"react-native": "0.72.0",
"react-native-iap": "12.11.0"
}
}
}

View File

@@ -295,7 +295,7 @@ const ShareView = ({ quicknote = false }) => {
try {
if (!globalThis["IS_MAIN_APP_RUNNING"]) {
await db.sync(false, false);
await db.sync({ type: "send", force: false });
} else {
console.log("main app running, skipping sync");
}

View File

@@ -1377,7 +1377,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "2.6.14",
"version": "2.6.17",
"license": "GPL-3.0-or-later",
"dependencies": {
"@aws-sdk/util-base64-browser": "^3.208.0",

View File

@@ -241,8 +241,8 @@ export class NoteContextMenuModel extends BaseProperties {
);
if (format === "html") {
return content
.replace(/(name="created-on" content=")(.+?)"/, '$1xxx"')
.replace(/(name="last-edited-on" content=")(.+?)"/, '$1xxx"');
.replace(/(name="created-at" content=")(.+?)"/, '$1xxx"')
.replace(/(name="updated-at" content=")(.+?)"/, '$1xxx"');
}
return content;
}

View File

@@ -8,8 +8,8 @@
content="This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1"
/>
<title>Test 1 - Notesnook</title>
<meta name="created-at" content="22-12-2023 11:33 AM" />
<meta name="updated-at" content="22-12-2023 11:33 AM" />
<meta name="created-at" content="xxx" />
<meta name="updated-at" content="xxx" />

View File

@@ -26,20 +26,25 @@ import { consumeReadableStream } from "../src/utils/stream";
import { xxhash64 } from "hash-wasm";
import path from "path";
const CHUNK_SIZE = 512 * 1024;
const CHUNK_SIZE = 512 * 1024 + 17;
test("chunked stream should create equal sized chunks", async (t) => {
const chunks = await consumeReadableStream(
(
Readable.toWeb(
createReadStream(
path.join(__dirname, "..", "__e2e__", "data", "importer-data.zip")
)
createReadStream(path.join(__dirname, "data", "35a4b0a78dbb9260"))
) as ReadableStream<Uint8Array>
).pipeThrough(new ChunkedStream(CHUNK_SIZE))
).pipeThrough(new ChunkedStream(CHUNK_SIZE, "copy"))
);
t.expect(await Promise.all(chunks.map((a) => xxhash64(a)))).toMatchObject([
"6234b76401d9eb97",
"338834da3f6500b2"
"9a3fa91d341b245d",
"c6b5d3ec17f14a5e",
"5163243faf462ce4",
"63aca6b8a7f68476",
"cd9d082fa3015bd3"
]);
t.expect(
await Promise.all(chunks.map((a) => a.byteOffset === 0))
).toMatchObject([true, true, true, true, true]);
});

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "2.6.15",
"version": "2.6.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "2.6.15",
"version": "2.6.17",
"license": "GPL-3.0-or-later",
"dependencies": {
"@aws-sdk/util-base64-browser": "^3.208.0",
@@ -210,13 +210,13 @@
"../../packages/core": {
"name": "@notesnook/core",
"version": "7.4.1",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -439,7 +439,7 @@
},
"../desktop": {
"name": "@notesnook/desktop",
"version": "2.6.14",
"version": "2.6.17",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "2.6.15",
"version": "2.6.17",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -381,13 +381,6 @@ function getDialogData(type: string) {
subtitle: "Please enter your vault password to continue.",
positiveButtonText: "Unlock"
};
case "ask_backup_password":
return {
title: "Encrypted backup",
subtitle:
"Please enter the password to decrypt and restore this backup.",
positiveButtonText: "Restore"
};
case "change_account_password":
return {
title: "Change account password",
@@ -463,6 +456,17 @@ export function showPasswordDialog(
));
}
export function showBackupPasswordDialog(
validate: (outputs: {
password?: string;
key?: string;
}) => boolean | Promise<boolean>
) {
return showDialog("BackupPasswordDialog", (Dialog, perform) => (
<Dialog onClose={() => perform(false)} validate={validate} />
));
}
export function showRecoveryKeyDialog() {
return showDialog("RecoveryKeyDialog", (Dialog, perform) => (
<Dialog onDone={() => perform(true)} />

View File

@@ -151,7 +151,7 @@ export async function exportNote(
: rawContent);
const exported = await note
.export(format === "pdf" ? "html" : format, content, disableTemplate)
.export(format === "pdf" ? "html" : format, content, !disableTemplate)
.catch((e: Error) => {
console.error(note.data, e);
showToast("error", `Failed to export note "${note.title}": ${e.message}`);

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
showBackupPasswordDialog,
showFeatureDialog,
showPasswordDialog,
showReminderDialog
@@ -26,7 +27,8 @@ import Config from "../utils/config";
import { hashNavigate, getCurrentHash } from "../navigation";
import { db } from "./db";
import { sanitizeFilename } from "@notesnook/common";
import { store as userstore } from "../stores/user-store";
import { useStore as useUserStore } from "../stores/user-store";
import { useStore as useSettingStore } from "../stores/setting-store";
import { showToast } from "../utils/toast";
import { SUBSCRIPTION_STATUS } from "./constants";
import { readFile, showFilePicker } from "../utils/file-picker";
@@ -78,8 +80,17 @@ export async function introduceFeatures() {
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
export async function createBackup() {
const encryptBackups =
userstore.get().isLoggedIn && Config.get("encryptBackups", false);
const { isLoggedIn } = useUserStore.getState();
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
const verified = encryptBackups || (await verifyAccount());
if (!verified) {
showToast("error", "Could not create a backup: user verification failed.");
return;
}
const encryptedBackups = isLoggedIn && encryptBackups;
const filename = sanitizeFilename(
`notesnook-backup-${getFormattedDate(Date.now())}`
@@ -101,7 +112,7 @@ export async function createBackup() {
await new ReadableStream({
start() {},
async pull(controller) {
for await (const file of db.backup!.export("web", encryptBackups)) {
for await (const file of db.backup!.export("web", encryptedBackups)) {
report({
text: `Saving chunk ${file.path}`
});
@@ -150,9 +161,9 @@ export async function restoreBackupFile(backupFile: File) {
const backup = JSON.parse(await readFile(backupFile));
if (backup.data.iv && backup.data.salt) {
await showPasswordDialog("ask_backup_password", async ({ password }) => {
if (!password) return false;
const error = await restoreWithProgress(backup, password);
await showBackupPasswordDialog(async ({ password, key }) => {
if (!password && !key) return false;
const error = await restoreWithProgress(backup, password, key);
return !error;
});
} else {
@@ -166,6 +177,7 @@ export async function restoreBackupFile(backupFile: File) {
type: "modal",
action: async (report) => {
let cachedPassword: string | undefined = undefined;
let cachedKey: string | undefined = undefined;
// const { read, totalFiles } = await Reader(backupFile);
const entries: ZipEntry[] = [];
let filesProcessed = 0;
@@ -183,20 +195,20 @@ export async function restoreBackupFile(backupFile: File) {
for (const entry of entries) {
const backup = JSON.parse(await entry.text());
if (backup.encrypted) {
if (!cachedPassword) {
const result = await showPasswordDialog(
"ask_backup_password",
async ({ password }) => {
if (!password) return false;
await db.backup?.import(backup, password);
if (!cachedPassword && !cachedKey) {
const result = await showBackupPasswordDialog(
async ({ password, key }) => {
if (!password && !key) return false;
await db.backup?.import(backup, password, key);
cachedPassword = password;
cachedKey = key;
return true;
}
);
if (!result) break;
} else await db.backup?.import(backup, cachedPassword);
} else await db.backup?.import(backup, cachedPassword, cachedKey);
} else {
await db.backup?.import(backup, null);
await db.backup?.import(backup);
}
report({
@@ -216,7 +228,8 @@ export async function restoreBackupFile(backupFile: File) {
async function restoreWithProgress(
backup: Record<string, unknown>,
password?: string
password?: string,
key?: string
) {
return await TaskManager.startTask<Error | void>({
title: "Restoring backup",
@@ -243,7 +256,7 @@ async function restoreWithProgress(
);
report({ text: `Restoring...` });
return restore(backup, password);
return restore(backup, password, key);
}
});
}
@@ -270,7 +283,7 @@ export function totalSubscriptionConsumed(user: User) {
export async function showUpgradeReminderDialogs() {
if (IS_TESTING) return;
const user = userstore.get().user;
const user = useUserStore.getState().user;
if (!user || !user.subscription || user.subscription?.expiry === 0) return;
const consumed = totalSubscriptionConsumed(user);
@@ -283,9 +296,13 @@ export async function showUpgradeReminderDialogs() {
}
}
async function restore(backup: Record<string, unknown>, password?: string) {
async function restore(
backup: Record<string, unknown>,
password?: string,
key?: string
) {
try {
await db.backup?.import(backup, password);
await db.backup?.import(backup, password, key);
showToast("success", "Backup restored!");
} catch (e) {
logger.error(e as Error, "Could not restore the backup");

View File

@@ -94,24 +94,21 @@ class Vault {
}
static lockNote(id) {
return new Promise(function lock(resolve) {
db.vault
.add(id)
.then(resolve)
.catch(({ message }) => {
switch (message) {
case db.vault.ERRORS.noVault:
return Vault.createVault();
case db.vault.ERRORS.vaultLocked:
return Vault.unlockVault();
default:
showToast("error", message);
console.error(message);
return false;
}
})
.then((result) => result && lock(resolve));
});
return db.vault
.add(id)
.then(() => true)
.catch(({ message }) => {
switch (message) {
case db.vault.ERRORS.noVault:
return Vault.createVault().then(() => Vault.lockNote(id));
case db.vault.ERRORS.vaultLocked:
return Vault.unlockVault().then(() => Vault.lockNote(id));
default:
showToast("error", message);
console.error(message);
return false;
}
});
}
static askPassword(action) {

View File

@@ -54,7 +54,7 @@ import { ScopedThemeProvider } from "../theme-provider";
import { Lightbox } from "../lightbox";
import { Allotment } from "allotment";
import { showToast } from "../../utils/toast";
import { getFormattedDate } from "@notesnook/common";
import { debounce, getFormattedDate } from "@notesnook/common";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
@@ -445,6 +445,38 @@ function EditorChrome(
isMobile: false
};
const editorMargins = useStore((store) => store.editorMargins);
const editorContainerRef = useRef<HTMLElement>(null);
const editorScrollRef = useRef<HTMLElement>(null);
useEffect(() => {
if (!editorScrollRef.current) return;
function onResize(
entries: ResizeObserverEntry[],
_observer: ResizeObserver
) {
const editor = editorContainerRef.current?.querySelector(
".ProseMirror"
) as HTMLElement | undefined;
const parent = editorScrollRef.current?.getBoundingClientRect();
const child = editorContainerRef.current?.getBoundingClientRect();
if (!parent || !child || !editor || entries.length <= 0) return;
const CONTAINER_MARGIN = 30;
const negativeSpace = Math.abs(
parent.left - child.left - CONTAINER_MARGIN
);
editor.style.marginLeft = `-${negativeSpace}px`;
editor.style.marginRight = `-${negativeSpace}px`;
editor.style.paddingLeft = `${negativeSpace}px`;
editor.style.paddingRight = `${negativeSpace}px`;
}
const observer = new ResizeObserver(debounce(onResize, 500));
observer.observe(editorScrollRef.current);
return () => {
observer.disconnect();
};
}, []);
if (headless) return <>{children}</>;
@@ -469,10 +501,12 @@ function EditorChrome(
<Toolbar />
<FlexScrollContainer
scrollRef={editorScrollRef}
className="editorScroll"
style={{ display: "flex", flexDirection: "column", flex: 1 }}
>
<Flex
ref={editorContainerRef}
variant="columnFill"
className="editor"
sx={{
@@ -481,7 +515,7 @@ function EditorChrome(
width: "100%"
}}
pl={6}
pr={2}
pr={6}
onClick={onRequestFocus}
>
{!isMobile && (

View File

@@ -141,6 +141,9 @@ function TipTap(props: TipTapProps) {
);
const dateFormat = useSettingsStore((store) => store.dateFormat);
const timeFormat = useSettingsStore((store) => store.timeFormat);
const markdownShortcuts = useSettingsStore(
(store) => store.markdownShortcuts
);
const { toolbarConfig } = useToolbarConfig();
const { isSearching, toggleSearch } = useSearch();
@@ -180,6 +183,7 @@ function TipTap(props: TipTapProps) {
}
}
},
enableInputRules: markdownShortcuts,
downloadOptions,
doubleSpacedLines,
dateFormat,
@@ -290,7 +294,14 @@ function TipTap(props: TipTapProps) {
},
getAttachmentData: onGetAttachmentData
};
}, [readonly, nonce, doubleSpacedLines, dateFormat, timeFormat]);
}, [
readonly,
nonce,
doubleSpacedLines,
dateFormat,
timeFormat,
markdownShortcuts
]);
const editor = useTiptap(
tiptapOptions,

View File

@@ -56,22 +56,27 @@ type GroupingMenuOptions = {
isUngrouped: boolean;
};
const groupByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
type: "button",
key: "groupBy",
title: "Group by",
icon: GroupBy.path,
menu: {
items: map(options, [
{ key: "none", title: "None" },
{ key: "default", title: "Default" },
{ key: "year", title: "Year" },
{ key: "month", title: "Month" },
{ key: "week", title: "Week" },
{ key: "abc", title: "A - Z" }
])
}
});
const groupByMenu: (options: GroupingMenuOptions) => MenuItem | null = (
options
) =>
options.groupingKey === "reminders"
? null
: {
type: "button",
key: "groupBy",
title: "Group by",
icon: GroupBy.path,
menu: {
items: map(options, [
{ key: "none", title: "None" },
{ key: "default", title: "Default" },
{ key: "year", title: "Year" },
{ key: "month", title: "Month" },
{ key: "week", title: "Week" },
{ key: "abc", title: "A - Z" }
])
}
};
const orderByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
type: "button",
@@ -90,12 +95,20 @@ const orderByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
{
key: "asc",
title:
options.groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
options.groupOptions.sortBy === "title"
? "A - Z"
: options.groupOptions.sortBy === "dueDate"
? "Earliest first"
: "Oldest - newest"
},
{
key: "desc",
title:
options.groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
options.groupOptions.sortBy === "title"
? "Z - A"
: options.groupOptions.sortBy === "dueDate"
? "Latest first"
: "Newest - oldest"
}
])
}
@@ -130,6 +143,11 @@ const sortByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
title: "Date modified",
isHidden: options.groupingKey !== "tags"
},
{
key: "dueDate",
title: "Due date",
isHidden: options.groupingKey !== "reminders"
},
{
key: "title",
title: "Title",
@@ -331,26 +349,26 @@ function GroupHeader(props: GroupHeaderProps) {
isUngrouped: false,
refresh
};
const groupBy = groupByMenu({
...menuOptions,
parentKey: "groupBy"
});
openMenu(
[
orderByMenu({
...menuOptions,
parentKey: "sortDirection"
}),
sortByMenu({
...menuOptions,
parentKey: "sortBy"
}),
groupByMenu({
...menuOptions,
parentKey: "groupBy"
})
],
{
title: "Group & sort"
}
);
const menuItems = [
orderByMenu({
...menuOptions,
parentKey: "sortDirection"
}),
sortByMenu({
...menuOptions,
parentKey: "sortBy"
})
];
if (groupBy) menuItems.push(groupBy);
openMenu(menuItems, {
title: groupBy ? "Group & sort" : "Sort"
});
}}
/>
)}

View File

@@ -84,7 +84,7 @@ function IconTag(props: IconTagProps) {
>
<Icon
size={11}
// color={styles?.icon?.color || (highlight ? "primary" : "icon")}
color={highlight ? "accent" : "icon"}
sx={{ ...styles?.icon, flexShrink: 0 }}
/>
<Text

View File

@@ -85,6 +85,7 @@ function Reminder(props: ReminderProps) {
gap: 1
}}
>
{reminder.disabled ? null : <PriorityIcon size={14} />}
{reminder.disabled ? (
<IconTag icon={ReminderOff} text={"Disabled"} testId={"disabled"} />
) : (
@@ -95,7 +96,6 @@ function Reminder(props: ReminderProps) {
testId={"reminder-time"}
/>
)}
{reminder.disabled ? null : <PriorityIcon size={14} />}
{reminder.mode === "repeat" && reminder.recurringMode && (
<IconTag
icon={Refresh}

View File

@@ -54,6 +54,7 @@ type FlexScrollContainerProps = {
id?: string;
className?: string;
style?: React.CSSProperties;
scrollRef?: React.Ref<HTMLElement>;
} & MacScrollbarProps;
export function FlexScrollContainer({
@@ -61,11 +62,13 @@ export function FlexScrollContainer({
children,
style,
className,
scrollRef,
...restProps
}: PropsWithChildren<FlexScrollContainerProps>) {
return (
<MacScrollbar
{...restProps}
ref={scrollRef}
id={id}
className={className}
style={style}

View File

@@ -0,0 +1,115 @@
/*
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 { useState } from "react";
import { Perform } from "../common/dialog-controller";
import Field from "../components/field";
import Dialog from "../components/dialog";
import { Box, Button } from "@theme-ui/components";
export type PromptDialogProps = {
onClose: Perform;
validate: (outputs: {
password?: string;
key?: string;
}) => boolean | Promise<boolean>;
};
export default function BackupPasswordDialog(props: PromptDialogProps) {
const { onClose, validate } = props;
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const [isEncryptionKey, setIsEncryptionKey] = useState(false);
return (
<Dialog
isOpen={true}
title={"Encrypted backup"}
description={
"Please enter the password to decrypt and restore this backup."
}
onClose={() => onClose(false)}
positiveButton={{
form: "backupPasswordForm",
type: "submit",
loading: isLoading,
disabled: isLoading,
text: "Restore"
}}
negativeButton={{ text: "Cancel", onClick: () => onClose(false) }}
>
<Box
id="backupPasswordForm"
as="form"
onSubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
try {
setIsLoading(true);
setError(undefined);
const key = formData.get("key") as string;
const password = formData.get("password") as string;
if (!key && !password) return;
if (await validate({ key, password })) {
onClose(true);
} else {
setError("Wrong password.");
}
} catch (e) {
setError((e as Error).message);
} finally {
setIsLoading(false);
}
}}
>
{isEncryptionKey ? (
<Field
required
autoFocus
data-test-id="dialog-key"
label="Encryption key"
type="password"
id="key"
name="key"
/>
) : (
<Field
required
autoFocus
data-test-id="dialog-password"
label="Password"
type="password"
autoComplete="current-password"
id="password"
name="password"
/>
)}
</Box>
<Button variant="anchor" onClick={() => setIsEncryptionKey((s) => !s)}>
{isEncryptionKey
? "Don't have encryption key? Use password."
: "Forgot password? Use encryption key."}
</Button>
</Dialog>
);
}

View File

@@ -91,20 +91,34 @@ const features: Record<FeatureKeys, Feature> = {
]
: [
{
title: "Daily trash cleanup interval",
title: "Sort reminders by due date",
subtitle:
"You can now set the trash cleanup interval to Daily in addition to Weekly, Monthly & Yearly."
"You can now sort your reminders by due date to quickly see which reminders are upcoming."
},
{
title: "Organized bulk exports",
title: "Restore backups using encryption key",
subtitle:
"Bulk exports are now automatically sorted/organized into folders based on your Notebook/Topic organization."
"If you forget your password but still have your recovery key, you can now use that to restore your backups."
},
{
title: "New domain for Monographs",
title: "Disable markdown in editor",
subtitle:
"Monographs will now be published to monogr.ph instead of monograph.notesnook.com. Don't worry, all your published notes will automatically redirect."
}
"If you find automatic markdown shortcuts annoying, you can now turn those off from Settings > Editor > Disable markdown shortcuts."
},
{
title: "Improved password reset",
subtitle:
"Resetting password should now be much more reliable and safer."
},
...(IS_DESKTOP_APP
? [
{
title: "Proxy settings",
subtitle:
"Desktop app now support setting a custom proxy url to route all network through it. Useful if you are behind a firewall and would like to bypass it."
}
]
: [])
],
cta: {
title: "Got it",

View File

@@ -52,6 +52,9 @@ const MigrationDialog = React.lazy(() => import("./migration-dialog"));
const EmailChangeDialog = React.lazy(() => import("./email-change-dialog"));
const AddTagsDialog = React.lazy(() => import("./add-tags-dialog"));
const ThemeDetailsDialog = React.lazy(() => import("./theme-details-dialog"));
const BackupPasswordDialog = React.lazy(
() => import("./backup-password-dialog")
);
export const Dialogs = {
AddNotebookDialog,
@@ -79,5 +82,6 @@ export const Dialogs = {
EmailChangeDialog,
AddTagsDialog,
SettingsDialog,
ThemeDetailsDialog
ThemeDetailsDialog,
BackupPasswordDialog
};

View File

@@ -63,7 +63,11 @@ function PasswordDialog(props) {
loading: isLoading,
disabled: isLoading
}}
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
negativeButton={{
text: "Cancel",
onClick: () => props.onClose(false),
disabled: isLoading
}}
>
<Box
id="passwordForm"

View File

@@ -19,11 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SettingsGroup } from "./types";
import { useStore as useUserStore } from "../../stores/user-store";
import { verifyAccount } from "../../common";
import { createBackup, verifyAccount } from "../../common";
import {
show2FARecoveryCodesDialog,
showMultifactorDialog,
showPasswordDialog
showPasswordDialog,
showRecoveryKeyDialog
} from "../../common/dialog-controller";
import { db } from "../../common/db";
import { showToast } from "../../utils/toast";
@@ -45,19 +46,22 @@ export const AuthenticationSettings: SettingsGroup[] = [
title: "Change password",
variant: "secondary",
action: async () => {
await createBackup();
const result = await showPasswordDialog(
"change_account_password",
async (data) => {
await db.user?.clearSessions();
return (
db.user?.changePassword(
(await db.user?.changePassword(
data.oldPassword,
data.newPassword
) || false
)) || false
);
}
);
if (result) showToast("success", "Account password changed!");
if (result) {
showToast("success", "Account password changed!");
await showRecoveryKeyDialog();
}
}
}
]

View File

@@ -43,17 +43,7 @@ export const BackupExportSettings: SettingsGroup[] = [
{
type: "button",
title: "Create backup",
action: async () => {
if (
!useUserStore.getState().isLoggedIn &&
useSettingStore.getState().encryptBackups
)
useSettingStore.getState().toggleEncryptBackups();
const verified =
useSettingStore.getState().encryptBackups ||
(await verifyAccount());
if (verified) await createBackup();
},
action: createBackup,
variant: "secondary"
}
]

View File

@@ -0,0 +1,53 @@
/*
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 { Button, Text } from "@theme-ui/components";
import { FlexScrollContainer } from "../../../components/scroll-container";
import { useSpellChecker } from "../../../hooks/use-spell-checker";
export function DictionaryWords() {
const words = useSpellChecker((store) => store.words);
const deleteWord = useSpellChecker((store) => store.deleteWord);
return (
<>
<FlexScrollContainer
suppressAutoHide
style={{
maxHeight: 400,
display: "flex",
flexDirection: "column"
}}
>
<Text variant="body" sx={{ my: 1 }}>
You have {words.length} custom dictionary words.
</Text>
{words.map((word) => (
<Button
variant="menuitem"
sx={{ textAlign: "left", p: 1 }}
onClick={() => deleteWord(word)}
>
{word}
</Button>
))}
</FlexScrollContainer>
</>
);
}

View File

@@ -29,6 +29,7 @@ import { useSpellChecker } from "../../hooks/use-spell-checker";
import { SpellCheckerLanguages } from "./components/spell-checker-languages";
import { CustomizeToolbar } from "./components/customize-toolbar";
import { DictionaryWords } from "./components/dictionary-words";
export const EditorSettings: SettingsGroup[] = [
{
@@ -100,7 +101,7 @@ symbols (e.g. 202305261253)`,
key: "double-spacing",
title: "Double spaced paragraphs",
description:
"Use double spacing between paragraphs and when you press Enter in the editor.",
"Use double spacing between paragraphs when you press Enter in the editor.",
onStateChange: (listener) =>
useSettingStore.subscribe((c) => c.doubleSpacedParagraphs, listener),
components: [
@@ -111,6 +112,27 @@ symbols (e.g. 202305261253)`,
useSettingStore.getState().toggleDoubleSpacedParagraphs()
}
]
},
{
key: "markdown-shortcuts",
title: "Markdown shortcuts",
description: `Markdown shortcuts are triggered whenever you input a specific character combination.
For example:
1. Typing '/date' adds the current Date
2. Wrapping something in '**' turns it into bold text
3. Typing '1.' automatically creates a numbered list.
4. etc.`,
onStateChange: (listener) =>
useSettingStore.subscribe((c) => c.markdownShortcuts, listener),
components: [
{
type: "toggle",
isToggled: () => useSettingStore.getState().markdownShortcuts,
toggle: () => useSettingStore.getState().toggleMarkdownShortcuts()
}
]
}
]
},
@@ -149,6 +171,16 @@ symbols (e.g. 202305261253)`,
component: SpellCheckerLanguages
}
]
},
{
key: "custom-dictionay-words",
title: "Custom dictionary words",
components: [
{
type: "custom",
component: DictionaryWords
}
]
}
]
},

View File

@@ -36,7 +36,8 @@ import {
Privacy,
Pro,
ShieldLock,
Sync
Sync,
Proxy
} from "../../components/icons";
import { Perform } from "../../common/dialog-controller";
import NavigationItem from "../../components/navigation-menu/navigation-item";
@@ -65,7 +66,6 @@ import {
import { AppearanceSettings } from "./appearance-settings";
import { debounce } from "@notesnook/common";
import { SubscriptionSettings } from "./subscription-settings";
import { alpha } from "@theme-ui/color";
import { ScopedThemeProvider } from "../../components/theme-provider";
type SettingsDialogProps = { onClose: Perform };

View File

@@ -115,7 +115,7 @@ What data is collected & when?`,
key: "custom-cors",
title: "Custom CORS proxy",
description:
"CORS proxy is required to directly download images from within the Notesnook app. It allows Notesnook to bypass browser restrictions by using a proxy. You can set a custom self-hosted proxy URL to increase your privacy",
"CORS proxy is required to directly download images from within the Notesnook app. It allows Notesnook to bypass browser restrictions by using a proxy. You can set a custom self-hosted proxy URL to increase your privacy.",
onStateChange: (listener) =>
useSettingStore.subscribe((s) => s.telemetry, listener),
components: [
@@ -144,6 +144,30 @@ What data is collected & when?`,
variant: "secondary"
}
]
},
{
key: "proxy-config",
title: "Proxy",
description: `Setup an HTTP/HTTPS/SOCKS proxy.
For example:
http://foobar:80
socks4://proxy.example.com
http://username:password@foobar:80
To remove the proxy, simply erase everything in the input.`,
onStateChange: (listener) =>
useSettingStore.subscribe((c) => c.proxyRules, listener),
components: [
{
type: "input",
inputType: "text",
defaultValue: () => useSettingStore.getState().proxyRules || "",
onChange: (value) => {
useSettingStore.getState().setProxyRules(value);
}
}
]
}
]
}

View File

@@ -36,4 +36,9 @@ declare global {
os?: () => NodeJS.Platform | "mas";
NativeNNCrypto?: new () => import("@notesnook/crypto").NNCrypto;
}
interface FileSystemFileHandle {
createSyncAccessHandle(options?: {
mode: "read-only" | "readwrite" | "readwrite-unsafe";
}): Promise<FileSystemSyncAccessHandle>;
}
}

View File

@@ -27,6 +27,7 @@ class SpellCheckerStore extends BaseStore<SpellCheckerStore> {
languages: Language[] = [];
enabled = true;
enabledLanguages: Language[] = [];
words: string[] = [];
toggleSpellChecker = async () => {
const enabled = this.get().enabled;
@@ -44,14 +45,19 @@ class SpellCheckerStore extends BaseStore<SpellCheckerStore> {
};
refresh = async () => {
console.log("SPELL CHECK", await desktop?.spellChecker.isEnabled.query());
this.set({
enabledLanguages:
(await desktop?.spellChecker.enabledLanguages.query()) || [],
languages: (await desktop?.spellChecker.languages.query()) || [],
enabled: await desktop?.spellChecker.isEnabled.query()
enabled: await desktop?.spellChecker.isEnabled.query(),
words: (await desktop?.spellChecker.words.query()) || []
});
};
deleteWord = async (word: string) => {
await desktop?.spellChecker.deleteWord.mutate(word);
await this.get().refresh();
};
}
const [useSpellChecker] = createStore(SpellCheckerStore);

View File

@@ -446,18 +446,19 @@ function reportProgress(
}
async function downloadFile(filename: string, requestOptions: RequestOptions) {
const { url, headers, chunkSize, signal } = requestOptions;
const handle = await streamablefs.readFile(filename);
if (
handle &&
handle.file.size === (await handle.size()) - handle.file.chunks * ABYTES
)
return true;
else if (handle) await handle.delete();
const attachment = db.attachments?.attachment(filename);
try {
const { url, headers, chunkSize, signal } = requestOptions;
const handle = await streamablefs.readFile(filename);
if (
handle &&
handle.file.size === (await handle.size()) - handle.file.chunks * ABYTES
)
return true;
else if (handle) await handle.delete();
const attachment = db.attachments?.attachment(filename);
reportProgress(
{ total: 100, loaded: 0 },
{ type: "download", hash: filename }

View File

@@ -24,6 +24,7 @@ import { expose, transfer } from "comlink";
class OriginPrivateFileStore implements IFileStorage {
private storage: IndexedDBKVStore;
private locks: Map<string, Promise<any>> = new Map();
constructor(
name: string,
private readonly directory: FileSystemDirectoryHandle
@@ -33,7 +34,9 @@ class OriginPrivateFileStore implements IFileStorage {
async clear(): Promise<void> {
for await (const [name] of this.directory) {
await this.directory.removeEntry(name, { recursive: true });
await this.safeOp(name, () =>
this.directory.removeEntry(name, { recursive: true })
);
}
await this.storage.clear();
}
@@ -47,33 +50,59 @@ class OriginPrivateFileStore implements IFileStorage {
return this.storage.delete(filename);
}
async writeChunk(chunkName: string, data: Uint8Array): Promise<void> {
const file = await this.directory.getFileHandle(chunkName, {
create: true
});
const syncHandle = await file.createSyncAccessHandle();
syncHandle.write(data);
syncHandle.close();
try {
await this.safeOp(chunkName, () =>
this.directory
.getFileHandle(chunkName, {
create: true
})
.then((file) => file.createSyncAccessHandle())
.then((handle) => {
handle.write(data);
handle.close();
})
);
} catch (e) {
console.error("Failed to write chunk", e);
}
}
async deleteChunk(chunkName: string) {
try {
await this.directory.removeEntry(chunkName);
await this.safeOp(chunkName, () => this.directory.removeEntry(chunkName));
} catch (e) {
console.error("Failed to delete chunk", e);
}
}
async readChunk(chunkName: string): Promise<Uint8Array | undefined> {
try {
const file = await this.directory.getFileHandle(chunkName);
const syncHandle = await file.createSyncAccessHandle();
const buffer = new Uint8Array(syncHandle.getSize());
syncHandle.read(buffer);
syncHandle.close();
return buffer;
if (Object.hasOwn(FileSystemSyncAccessHandle.prototype, "mode")) {
return readFile(this.directory, chunkName);
}
// OPFS currently does not support multiple readers on a single file
// on all browsers so we wait for the file handle to be released before
// continuing. This is temporary until all browsers start supporting
// the read-only mode.
return await this.safeOp(chunkName, () =>
readFile(this.directory, chunkName)
);
} catch (e) {
console.error("Failed to read chunk", e);
return;
}
}
private async safeOp<T>(chunkName: string, createPromise: () => Promise<T>) {
const lock = this.locks.get(chunkName);
if (lock) await lock;
const promise = createPromise();
this.locks.set(chunkName, promise);
return await promise.finally(() => this.locks.delete(chunkName));
}
}
const fileStores: Map<string, OriginPrivateFileStore> = new Map();
@@ -117,3 +146,12 @@ const workerModule = {
expose(workerModule);
export type OriginPrivateFileStoreWorkerType = typeof workerModule;
async function readFile(directory: FileSystemDirectoryHandle, name: string) {
const file = await directory.getFileHandle(name);
const handle = await file.createSyncAccessHandle({ mode: "read-only" });
const buffer = new Uint8Array(handle.getSize());
handle.read(buffer);
handle.close();
return buffer;
}

View File

@@ -298,7 +298,11 @@ class AppStore extends BaseStore {
this.updateSyncStatus("syncing");
try {
const result = await db.sync(full, force, lastSynced);
const result = await db.sync({
type: full ? "full" : "send",
force,
serverLastSynced: lastSynced
});
if (!result) return this.updateSyncStatus("failed");
this.updateSyncStatus("completed", true);
@@ -307,7 +311,6 @@ class AppStore extends BaseStore {
if (pendingSync) {
logger.info("Running pending sync", pendingSync);
pendingSync = false;
await this.get().sync(pendingSync.full, false);
}
} catch (err) {

View File

@@ -72,9 +72,8 @@ class AttachmentStore extends BaseStore {
await createWriteStream("attachments.zip", {
signal: abortController.signal
})
);
this.set((state) => (state.status = undefined));
)
.finally(() => this.set((state) => (state.status = undefined)));
};
cancel = async () => {

View File

@@ -290,12 +290,14 @@ class EditorStore extends BaseStore {
state: SESSION_STATES.new
};
});
noteStore.setSelectedNote(0);
this.toggleProperties(false);
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
setTimeout(() => appStore.setIsEditorOpen(false), 100);
setDocumentTitle();
setTimeout(() => {
noteStore.setSelectedNote(0);
this.toggleProperties(false);
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
appStore.setIsEditorOpen(false);
setDocumentTitle();
}, 100);
};
setTitle = (noteId, title) => {

View File

@@ -116,7 +116,7 @@ class NoteStore extends BaseStore {
unlock = async (id) => {
return await Vault.unlockNote(id).then(async (res) => {
if (editorStore.get().session.id === id)
await editorStore.openSession(id);
await editorStore.clearSession(true);
this.refreshItem(id);
return res;
});

View File

@@ -37,7 +37,13 @@ class ReminderStore extends BaseStore {
refresh = (reset = true) => {
const reminders = db.reminders.all;
this.set((state) => (state.reminders = groupReminders(reminders)));
this.set(
(state) =>
(state.reminders = groupReminders(
reminders,
db.settings.getGroupOptions("reminders")
))
);
if (reset) {
resetReminders(reminders);
notestore.refresh();

View File

@@ -38,6 +38,7 @@ class SettingStore extends BaseStore {
PATHS.backupsDirectory
);
doubleSpacedParagraphs = Config.get("doubleSpacedLines", true);
markdownShortcuts = Config.get("markdownShortcuts", true);
notificationsSettings = Config.get("notifications", { reminder: true });
zoomFactor = 1.0;
@@ -58,6 +59,10 @@ class SettingStore extends BaseStore {
desktopIntegrationSettings = undefined;
autoUpdates = true;
isFlatpak = false;
/**
* @type {string|undefined}
*/
proxyRules = undefined;
refresh = async () => {
this.set({
@@ -70,7 +75,8 @@ class SettingStore extends BaseStore {
await desktop?.integration.desktopIntegration.query(),
privacyMode: await desktop?.integration.privacyMode.query(),
zoomFactor: await desktop?.integration.zoomFactor.query(),
autoUpdates: await desktop?.updater.autoUpdates.query()
autoUpdates: await desktop?.updater.autoUpdates.query(),
proxyRules: await desktop?.integration.proxyRules.query()
});
};
@@ -99,6 +105,11 @@ class SettingStore extends BaseStore {
this.set({ zoomFactor });
};
setProxyRules = async (proxyRules) => {
await desktop?.integration.setProxyRules.mutate(proxyRules);
this.set({ proxyRules });
};
setEncryptBackups = (encryptBackups) => {
this.set({ encryptBackups });
Config.set("encryptBackups", encryptBackups);
@@ -156,6 +167,14 @@ class SettingStore extends BaseStore {
Config.set("doubleSpacedLines", !doubleSpacedParagraphs);
};
toggleMarkdownShortcuts = (toggleState) => {
this.set((state) => {
state.markdownShortcuts =
toggleState !== undefined ? toggleState : !state.markdownShortcuts;
Config.set("markdownShortcuts", state.markdownShortcuts);
});
};
toggleTelemetry = () => {
const telemetry = this.get().telemetry;
this.set({ telemetry: !telemetry });

View File

@@ -26,6 +26,7 @@ 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";
let logger: typeof _logger;
async function initalizeLogger(persistence: DatabasePersistence = "db") {
@@ -36,19 +37,23 @@ async function initalizeLogger(persistence: DatabasePersistence = "db") {
async function downloadLogs() {
if (!logManager) return;
const allLogs = await logManager.get();
let i = 0;
const textEncoder = new TextEncoder();
await new ReadableStream<ZipFile>({
pull(controller) {
for (const log of allLogs) {
controller.enqueue({
path: log.key,
data: textEncoder.encode(
(log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
)
});
const log = allLogs[i++];
if (!log) {
controller.close();
return;
}
controller.enqueue({
path: sanitizeFilename(log.key, { replacement: "-" }),
data: textEncoder.encode(
(log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
)
});
}
})
.pipeThrough(createZipStream())

View File

@@ -40,46 +40,51 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
super({
start() {},
async pull(controller) {
if (signal?.aborted) {
controller.close();
return;
}
try {
if (signal?.aborted) {
controller.close();
return;
}
onProgress && onProgress(index);
const attachment = attachments[index++];
onProgress && onProgress(index);
const attachment = attachments[index++];
await db.fs?.downloadFile(
GROUP_ID,
attachment.metadata.hash,
attachment.chunkSize,
attachment.metadata
);
await db.fs?.downloadFile(
GROUP_ID,
attachment.metadata.hash,
attachment.chunkSize,
attachment.metadata
);
const key = await db.attachments?.decryptKey(attachment.key);
const file = await lazify(
import("../../interfaces/fs"),
({ decryptFile }) =>
decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
})
);
const key = await db.attachments?.decryptKey(attachment.key);
const file = await lazify(
import("../../interfaces/fs"),
({ decryptFile }) =>
decryptFile(attachment.metadata.hash, {
key,
iv: attachment.iv,
name: attachment.metadata.filename,
type: attachment.metadata.type,
isUploaded: !!attachment.dateUploaded
})
);
if (file) {
const filePath: string = attachment.metadata.filename;
controller.enqueue({
path: makeUniqueFilename(filePath, counters),
data: new Uint8Array(await file.arrayBuffer())
});
} else {
controller.error(new Error("Failed to decrypt file."));
}
if (index === attachments.length) {
controller.close();
if (file) {
const filePath: string = attachment.metadata.filename;
controller.enqueue({
path: makeUniqueFilename(filePath, counters),
data: new Uint8Array(await file.arrayBuffer())
});
} else {
controller.error(new Error("Failed to decrypt file."));
}
} catch (e) {
console.error(e);
controller.error(e);
} finally {
if (index === attachments.length) {
controller.close();
}
}
}
});

View File

@@ -55,7 +55,11 @@ export class ChunkedStream extends TransformStream<Uint8Array, Uint8Array> {
}
},
flush(controller) {
if (backBuffer) controller.enqueue(backBuffer);
if (backBuffer) {
const buffer =
mode === "copy" ? new Uint8Array(backBuffer) : backBuffer;
controller.enqueue(buffer);
}
}
});
}

View File

@@ -358,7 +358,7 @@ function RecoveryKeyMethod(props: BaseRecoveryComponentProps<"method:key">) {
const user = await db.user?.getUser();
if (!user) throw new Error("User not authenticated");
await db.storage?.write(`_uk_@${user.email}@_k`, form.recoveryKey);
await db.sync(true, true);
await db.sync({ type: "fetch", force: true });
navigate("backup");
}}
>
@@ -507,7 +507,7 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
if (formData?.backupFile) {
await restoreBackupFile(formData?.backupFile);
await db.sync(true, true);
await db.sync({ type: "full", force: true });
}
navigate("final");

View File

@@ -0,0 +1,7 @@
- Restore backups using encryption key in addition to password
- Toggle markdown shortcuts in the editor
- Improved user experience and reliability when changing account password
- Fixed doing a search in attachment manager causes a crash
- Bug fixes and minor improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Minor bug fixes
Thank you for using Notesnook!

View File

@@ -26,13 +26,13 @@
"name": "@notesnook/core",
"version": "7.4.1",
"dev": true,
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -144,12 +144,11 @@
"@notesnook/core": {
"version": "file:../core",
"requires": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/crypto": "file:../crypto",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"@types/html-to-text": "^9.0.0",
"@types/katex": "^0.16.1",
"@types/prismjs": "^1.26.0",

View File

@@ -223,7 +223,7 @@ test(
const handler = vitest.fn();
deviceB.eventManager.subscribe(EVENTS.syncProgress, handler);
await deviceB.sync(true);
await deviceB.sync({ type: "full" });
expect(handler).not.toHaveBeenCalled();
@@ -415,7 +415,7 @@ async function initializeDevice(id, capabilities = []) {
await device.user.resetUser(false);
await device.sync(true, false);
await device.sync({ type: "full" });
console.timeEnd(`Init ${id}`);
return device;
@@ -448,7 +448,13 @@ function syncAndWait(deviceA, deviceB, force = false) {
(full, force, lastSynced) => {
console.log("sync requested by device A", full, force, lastSynced);
ref2.unsubscribe();
deviceB.sync(full, force, lastSynced).catch(reject);
deviceB
.sync({
type: full ? "full" : "send",
force,
serverLastSynced: lastSynced
})
.catch(reject);
}
);
@@ -466,6 +472,6 @@ function syncAndWait(deviceA, deviceB, force = false) {
deviceB.syncer.sync.syncing
);
deviceA.sync(true, force).catch(reject);
deviceA.sync({ type: "full", force }).catch(reject);
});
}

View File

@@ -17,8 +17,9 @@ 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 { authenticator } from "otplib";
import { databaseTest } from "../__tests__/utils";
import { login, user } from "./utils";
import { login, USER } from "./utils";
import { test, expect } from "vitest";
// test("signup user and check for token", async () => {
@@ -50,7 +51,55 @@ test(
await login(db);
const userData = await db.user.getUser();
expect(userData.email).toBe(user.email);
expect(userData.email).toBe(USER.email);
}),
30000
);
test(
"login user after entering invalid mfa once",
() =>
databaseTest().then(async (db) => {
await db.user.authenticateEmail(USER.email);
await expect(
db.user.authenticateMultiFactorCode(201022, "app")
).rejects.toThrowError(
/Please provide a valid multi-factor authentication/
);
const token = authenticator.generate(USER.totpSecret);
await db.user.authenticateMultiFactorCode(token, "app");
await expect(
db.user.authenticatePassword(USER.email, USER.password, USER.hashed)
).resolves.toBeFalsy();
await expect(db.user.tokenManager.getToken()).resolves.toBeDefined();
}),
30000
);
test(
"login user after entering incorrect password once",
() =>
databaseTest().then(async (db) => {
await db.user.authenticateEmail(USER.email);
const token = authenticator.generate(USER.totpSecret);
await db.user.authenticateMultiFactorCode(token, "app");
await expect(
db.user.authenticatePassword(USER.email, "wrong_password")
).rejects.toThrowError(/Password is incorrect./);
await db.user.authenticatePassword(
USER.email,
USER.password,
USER.hashed
);
await expect(db.user.tokenManager.getToken()).resolves.toBeDefined();
}),
30000
);

View File

@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { authenticator } from "otplib";
export const user = {
export const USER = {
email: process.env.USER_EMAIL,
password: process.env.USER_PASSWORD,
hashed: process.env.USER_HASHED_PASSWORD,
totpSecret: process.env.USER_TOTP_SECRET
};
export async function login(db) {
export async function login(db, user = USER) {
await db.user.authenticateEmail(user.email);
const token = authenticator.generate(user.totpSecret);

View File

@@ -7,13 +7,13 @@
"": {
"name": "@notesnook/core",
"version": "7.4.1",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -494,9 +494,9 @@
}
},
"node_modules/@microsoft/signalr": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.10.tgz",
"integrity": "sha512-tOEn32i5EatAx4sZbzmLgcBc2VbKQmx+F4rI2/Ioq2MnBaYcFxbDzOoZgISIS4IR9H1ij/sKoU8zQOAFC8GJKg==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.0.tgz",
"integrity": "sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
@@ -505,23 +505,6 @@
"ws": "^7.4.5"
}
},
"node_modules/@microsoft/signalr-protocol-msgpack": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-7.0.10.tgz",
"integrity": "sha512-iZacNFQ3+BT3wZjFN2qcuQQJWK0ZlyCek4plWw1QrFqqOMBYEwPY4BCbLcwNZcTiOpTK65es1CCf3Yxb6lwlVQ==",
"dependencies": {
"@microsoft/signalr": ">=7.0.10",
"@msgpack/msgpack": "^2.7.0"
}
},
"node_modules/@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@notesnook/crypto": {
"resolved": "../crypto",
"link": true
@@ -598,9 +581,9 @@
}
},
"node_modules/@streetwriters/showdown": {
"version": "3.0.4-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.4-alpha.tgz",
"integrity": "sha512-R2UJzMXyJz312RxekXGQIQvLsdZ0eSa4Z7dRXzf/fL2XsPPtyEzkZce0RoZn/mmKfDym5XKnjcVxnF3hXgjVaw==",
"version": "3.0.5-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.5-alpha.tgz",
"integrity": "sha512-jD9JFhxLDx6XeyZOLVB0zWtwGduwNiFpxn5rxu6ThyKyWGnu1O+L1w04WLC1L56pyEhypr3Tsk24dzo2Se/50g==",
"bin": {
"showdown": "bin/showdown.js"
},
@@ -3182,9 +3165,9 @@
}
},
"@microsoft/signalr": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.10.tgz",
"integrity": "sha512-tOEn32i5EatAx4sZbzmLgcBc2VbKQmx+F4rI2/Ioq2MnBaYcFxbDzOoZgISIS4IR9H1ij/sKoU8zQOAFC8GJKg==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.0.tgz",
"integrity": "sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==",
"requires": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
@@ -3193,20 +3176,6 @@
"ws": "^7.4.5"
}
},
"@microsoft/signalr-protocol-msgpack": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr-protocol-msgpack/-/signalr-protocol-msgpack-7.0.10.tgz",
"integrity": "sha512-iZacNFQ3+BT3wZjFN2qcuQQJWK0ZlyCek4plWw1QrFqqOMBYEwPY4BCbLcwNZcTiOpTK65es1CCf3Yxb6lwlVQ==",
"requires": {
"@microsoft/signalr": ">=7.0.10",
"@msgpack/msgpack": "^2.7.0"
}
},
"@msgpack/msgpack": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ=="
},
"@notesnook/crypto": {
"version": "file:../crypto",
"requires": {
@@ -3278,9 +3247,9 @@
}
},
"@streetwriters/showdown": {
"version": "3.0.4-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.4-alpha.tgz",
"integrity": "sha512-R2UJzMXyJz312RxekXGQIQvLsdZ0eSa4Z7dRXzf/fL2XsPPtyEzkZce0RoZn/mmKfDym5XKnjcVxnF3hXgjVaw=="
"version": "3.0.5-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.5-alpha.tgz",
"integrity": "sha512-jD9JFhxLDx6XeyZOLVB0zWtwGduwNiFpxn5rxu6ThyKyWGnu1O+L1w04WLC1L56pyEhypr3Tsk24dzo2Se/50g=="
},
"@tootallnate/once": {
"version": "2.0.0",

View File

@@ -35,14 +35,14 @@
"build": "tsc",
"watch": "tsc --watch",
"test:e2e": "cross-env IS_E2E=true vitest run",
"test": "vitest run"
"test": "vitest run",
"postinstall": "patch-package"
},
"dependencies": {
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@microsoft/signalr": "^8.0.0",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.4-alpha",
"@streetwriters/showdown": "^3.0.5-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",

View File

@@ -0,0 +1,52 @@
diff --git a/node_modules/@microsoft/signalr/dist/browser/signalr.js b/node_modules/@microsoft/signalr/dist/browser/signalr.js
index 86f7968..9bc9745 100644
--- a/node_modules/@microsoft/signalr/dist/browser/signalr.js
+++ b/node_modules/@microsoft/signalr/dist/browser/signalr.js
@@ -347,7 +347,7 @@ class Platform {
// Node apps shouldn't have a window object, but WebWorkers don't either
// so we need to check for both WebWorker and window
static get isNode() {
- return typeof process !== "undefined" && process.release && process.release.name === "node";
+ return typeof process !== "undefined" && process.release && process.release.name === "node" && process.type !== "renderer";
}
}
/** @private */
diff --git a/node_modules/@microsoft/signalr/dist/cjs/Utils.js b/node_modules/@microsoft/signalr/dist/cjs/Utils.js
index 2db55f7..228d318 100644
--- a/node_modules/@microsoft/signalr/dist/cjs/Utils.js
+++ b/node_modules/@microsoft/signalr/dist/cjs/Utils.js
@@ -45,7 +45,7 @@ class Platform {
// Node apps shouldn't have a window object, but WebWorkers don't either
// so we need to check for both WebWorker and window
static get isNode() {
- return typeof process !== "undefined" && process.release && process.release.name === "node";
+ return typeof process !== "undefined" && process.release && process.release.name === "node" && process.type !== "renderer";
}
}
exports.Platform = Platform;
diff --git a/node_modules/@microsoft/signalr/dist/esm/Utils.js b/node_modules/@microsoft/signalr/dist/esm/Utils.js
index a8962ee..2fd2558 100644
--- a/node_modules/@microsoft/signalr/dist/esm/Utils.js
+++ b/node_modules/@microsoft/signalr/dist/esm/Utils.js
@@ -41,7 +41,7 @@ export class Platform {
// Node apps shouldn't have a window object, but WebWorkers don't either
// so we need to check for both WebWorker and window
static get isNode() {
- return typeof process !== "undefined" && process.release && process.release.name === "node";
+ return typeof process !== "undefined" && process.release && process.release.name === "node" && process.type !== "renderer";
}
}
/** @private */
diff --git a/node_modules/@microsoft/signalr/dist/webworker/signalr.js b/node_modules/@microsoft/signalr/dist/webworker/signalr.js
index 86f7968..9bc9745 100644
--- a/node_modules/@microsoft/signalr/dist/webworker/signalr.js
+++ b/node_modules/@microsoft/signalr/dist/webworker/signalr.js
@@ -347,7 +347,7 @@ class Platform {
// Node apps shouldn't have a window object, but WebWorkers don't either
// so we need to check for both WebWorker and window
static get isNode() {
- return typeof process !== "undefined" && process.release && process.release.name === "node";
+ return typeof process !== "undefined" && process.release && process.release.name === "node" && process.type !== "renderer";
}
}
/** @private */

View File

@@ -33,7 +33,6 @@ import Constants from "../utils/constants";
import { EV, EVENTS } from "../common";
import Settings from "./settings";
import Migrations from "./migrations";
import Outbox from "./outbox";
import UserManager from "./user-manager";
import http from "../utils/http";
import Monographs from "./monographs";
@@ -90,7 +89,6 @@ class Database {
this.backup = new Backup(this);
this.settings = new Settings(this);
this.migrations = new Migrations(this);
this.outbox = new Outbox(this);
this.monographs = new Monographs(this);
this.offers = new Offers();
this.debug = new Debug();
@@ -115,7 +113,7 @@ class Database {
this
);
EV.subscribe(EVENTS.attachmentDeleted, async (attachment) => {
await this.fs.cancel(attachment.metadata.hash);
await this.fs.cancel(attachment.metadata?.hash);
});
EV.subscribe(EVENTS.userLoggedOut, async () => {
await this.monographs.deinit();
@@ -127,7 +125,6 @@ class Database {
await this.initCollections();
await this.outbox.init();
await this.migrations.init();
this.isInitialized = true;
if (this.migrations.required()) {
@@ -275,8 +272,17 @@ class Database {
return (await this.storage.read("lastSynced")) || 0;
}
sync(full = true, force = false, lastSynced = null) {
return this.syncer.start(full, force, lastSynced);
/**
*
* @param {{
* type: "full" | "fetch" | "send";
* force?: boolean;
* serverLastSynced?: number;
* }} options
* @returns
*/
sync(options) {
return this.syncer.start(options);
}
/**

View File

@@ -67,10 +67,10 @@ export default class Lookup {
}
attachments(array, query) {
return search(
array,
query,
(n) => `${n.metadata.filename} ${n.metadata.type} ${n.metadata.hash}`
return search(array, query, (n) =>
n.metadata
? `${n.metadata.filename} ${n.metadata.type} ${n.metadata.hash}`
: ""
);
}

View File

@@ -1,57 +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/>.
*/
class Outbox {
/**
*
* @param {import("./index").default} db
*/
constructor(db) {
this._db = db;
this.outbox = {};
}
async init() {
this.outbox = (await this._db.storage.read("outbox")) || {};
for (var id in this.outbox) {
const data = this.outbox[id];
switch (id) {
case "reset_password":
case "change_password":
if (await this._db.user._updatePassword(id, data))
await this.delete(id);
break;
}
}
}
async add(id, data, action) {
this.outbox[id] = data;
await this._db.storage.write("outbox", this.outbox);
await action();
await this.delete(id);
}
delete(id) {
delete this.outbox[id];
return this._db.storage.write("outbox", this.outbox);
}
}
export default Outbox;

View File

@@ -99,8 +99,10 @@ class Settings {
? "dateDeleted"
: key === "tags"
? "dateCreated"
: key === "reminders"
? "dueDate"
: "dateEdited",
sortDirection: "desc"
sortDirection: key === "reminders" ? "asc" : "desc"
}
);
}

View File

@@ -31,7 +31,6 @@ import * as signalr from "@microsoft/signalr";
import Merger from "./merger";
import Conflicts from "./conflicts";
import { AutoSync } from "./auto-sync";
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
import { logger } from "../../logger";
import { Mutex } from "async-mutex";
import { migrateItem } from "../../migrations";
@@ -53,10 +52,10 @@ export default class SyncManager {
this._db = db;
}
async start(full, force, serverLastSynced) {
async start(options) {
try {
await this.sync.autoSync.start();
await this.sync.start(full, force, serverLastSynced);
await this.sync.start(options);
return true;
} catch (e) {
var isHubException = e.message.includes("HubException:");
@@ -144,7 +143,7 @@ class Sync {
}
}
})
.withHubProtocol(new MessagePackHubProtocol({ ignoreUndefined: true }))
.withHubProtocol(new signalr.JsonHubProtocol())
.build();
this.connection.serverTimeoutInMilliseconds = 60 * 1000 * 5;
EV.subscribe(EVENTS.userLoggedOut, async () => {
@@ -176,18 +175,20 @@ class Sync {
/**
*
* @param {boolean} full
* @param {boolean} force
* @param {number} serverLastSynced
* @param {{
* type: "full" | "fetch" | "send";
* force?: boolean;
* serverLastSynced?: number;
* }} options
*/
async start(full, force, serverLastSynced) {
async start(options) {
if (!(await checkSyncStatus(SYNC_CHECK_IDS.sync))) {
await this.connection.stop();
return;
}
if (!(await this.db.user.getUser())) return;
this.logger.info("Starting sync", { full, force, serverLastSynced });
this.logger.info("Starting sync", options);
this.connection.onclose((error) => {
this.db.eventManager.publish(EVENTS.syncAborted);
@@ -196,15 +197,21 @@ class Sync {
throw new Error("Connection closed.");
});
const { lastSynced, oldLastSynced } = await this.init(force);
const { lastSynced, oldLastSynced } = await this.init(options.force);
this.logger.info("Initialized sync", { lastSynced, oldLastSynced });
const newLastSynced = Date.now();
const serverResponse = full ? await this.fetch(lastSynced) : null;
const serverResponse =
options.type === "fetch" || options.type === "full"
? await this.fetch(lastSynced)
: null;
this.logger.info("Data fetched", serverResponse);
if (await this.send(lastSynced, force, newLastSynced)) {
if (
(options.type === "send" || options.type === "full") &&
(await this.send(lastSynced, options.force, newLastSynced))
) {
this.logger.info("New data sent");
await this.stop(newLastSynced);
} else if (serverResponse) {
@@ -212,7 +219,7 @@ class Sync {
await this.stop(serverResponse.lastSynced);
} else {
this.logger.info("Nothing to do.");
await this.stop(serverLastSynced || oldLastSynced);
await this.stop(options.serverLastSynced || oldLastSynced);
}
if (!(await checkSyncStatus(SYNC_CHECK_IDS.autoSync))) {

View File

@@ -165,10 +165,17 @@ class Merger {
if (remoteItem.deleted)
return this._db.attachments.merge(null, remoteItem);
// rare case but if remote attachment doesn't have metadata it's probably broken so we should just skip it in this case.
if (!remoteItem.metadata) break;
const localItem = this._db.attachments.attachment(
remoteItem.metadata.hash
remoteItem.metadata?.hash
);
if (localItem && localItem.dateUploaded !== remoteItem.dateUploaded) {
if (
localItem &&
localItem.metadata &&
localItem.dateUploaded !== remoteItem.dateUploaded
) {
const noteIds = localItem.noteIds.slice();
const isRemoved = await this._db.attachments.remove(
localItem.metadata.hash,

View File

@@ -129,39 +129,43 @@ class UserManager {
) {
if (!email || !password) throw new Error("email & password are required.");
const token = await this.tokenManager.getAccessToken();
if (!token) throw new Error("Unauthorized.");
const token = await this.tokenManager.getToken();
if (!token) throw new Error("No token found.");
email = email.toLowerCase();
if (!hashedPassword) {
hashedPassword = await this._storage.hash(password, email);
}
try {
await this.tokenManager.saveToken(
await http.post(
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
{
grant_type: "mfa_password",
client_id: "notesnook",
scope: "notesnook.sync offline_access IdentityServerApi",
password: hashedPassword
},
token.access_token
)
);
await this.tokenManager.saveToken(
await http.post(
`${constants.AUTH_HOST}${ENDPOINTS.token}`,
{
grant_type: "mfa_password",
client_id: "notesnook",
scope: "notesnook.sync offline_access IdentityServerApi",
password: hashedPassword
},
token
)
);
const user = await this.fetchUser();
if (!user) throw new Error("Unauthorized.");
const user = await this.fetchUser();
if (!user) throw new Error("Unauthorized.");
if (!sessionExpired) {
await this._storage.write("lastSynced", 0);
}
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
password,
salt: user.salt
});
if (!sessionExpired) {
await this._storage.write("lastSynced", 0);
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
password,
salt: user.salt
});
EV.publish(EVENTS.userLoggedIn, user);
} catch (e) {
await this.tokenManager.saveToken(token);
throw e;
}
EV.publish(EVENTS.userLoggedIn, user);
}
/**
@@ -454,44 +458,45 @@ class UserManager {
const attachmentsKey = await this.getAttachmentsKey();
data.encryptionKey = data.encryptionKey || (await this.getEncryptionKey());
await this._db.outbox.add(type, data, async () => {
if (data.encryptionKey) await this._db.sync(true, true);
await this._storage.deriveCryptoKey(`_uk_@${email}`, {
password: new_password,
salt
});
await this.clearSessions();
if (!(await this.resetUser(false))) return;
if (data.encryptionKey) await this._db.sync({ type: "fetch", force: true });
if (attachmentsKey) {
const userEncryptionKey = await this.getEncryptionKey();
if (!userEncryptionKey) return;
user.attachmentsKey = await this._storage.encrypt(
userEncryptionKey,
JSON.stringify(attachmentsKey)
);
await this.updateUser(user);
}
await this._db.sync(false, true);
if (old_password)
old_password = await this._storage.hash(old_password, email);
if (new_password)
new_password = await this._storage.hash(new_password, email);
await http.patch(
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
{
type,
old_password,
new_password
},
token
);
await this._storage.deriveCryptoKey(`_uk_@${email}`, {
password: new_password,
salt
});
if (!(await this.resetUser(false))) return;
await this._db.sync({ type: "send", force: true });
if (attachmentsKey) {
const userEncryptionKey = await this.getEncryptionKey();
if (!userEncryptionKey) return;
user.attachmentsKey = await this._storage.encrypt(
userEncryptionKey,
JSON.stringify(attachmentsKey)
);
await this.updateUser(user);
}
if (old_password)
old_password = await this._storage.hash(old_password, email);
if (new_password)
new_password = await this._storage.hash(new_password, email);
await http.patch(
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
{
type,
old_password,
new_password
},
token
);
return true;
}
}

View File

@@ -40,7 +40,7 @@ export default class Attachments extends Collection {
async ({ success, filename, groupId, eventData }) => {
if (!success || !eventData || !eventData.readOnDownload) return;
const attachment = this.attachment(filename);
if (!attachment) return;
if (!attachment || !attachment.metadata) return;
const src = await this.read(filename, getOutputType(attachment));
if (!src) return;
@@ -108,7 +108,7 @@ export default class Attachments extends Collection {
if (!attachmentArg.hash) throw new Error("Please provide attachment hash.");
const oldAttachment =
this.all.find((a) => a.metadata.hash === attachmentArg.hash) || {};
this.all.find((a) => a.metadata?.hash === attachmentArg.hash) || {};
let id = oldAttachment.id || getId();
const noteIds = oldAttachment.noteIds || [];
@@ -205,7 +205,7 @@ export default class Attachments extends Collection {
async remove(hashOrId, localOnly) {
const attachment = this.attachment(hashOrId);
if (!attachment) return false;
if (!attachment || !attachment.metadata) return false;
if (!localOnly && !(await this._canDetach(attachment)))
throw new Error("This attachment is inside a locked note.");
@@ -267,7 +267,7 @@ export default class Attachments extends Collection {
}
exists(hash) {
const attachment = this.all.find((a) => a.metadata.hash === hash);
const attachment = this.all.find((a) => a.metadata?.hash === hash);
return !!attachment;
}
@@ -277,7 +277,7 @@ export default class Attachments extends Collection {
* @returns {Promise<string>} dataurl formatted string
*/
async read(hash, outputType) {
const attachment = this.all.find((a) => a.metadata.hash === hash);
const attachment = this.all.find((a) => a.metadata?.hash === hash);
if (!attachment) return;
const key = await this.decryptKey(attachment.key);
@@ -302,7 +302,7 @@ export default class Attachments extends Collection {
attachment(hashOrId) {
return this.all.find(
(a) => a.id === hashOrId || a.metadata.hash === hashOrId
(a) => a.id === hashOrId || a.metadata?.hash === hashOrId
);
}
@@ -348,8 +348,9 @@ export default class Attachments extends Collection {
async downloadMedia(noteId, hashesToLoad) {
const attachments = this.media.filter(
(attachment) =>
!!attachment.metadata &&
hasItem(attachment.noteIds, noteId) &&
(!hashesToLoad || hasItem(hashesToLoad, attachment.metadata.hash))
(!hashesToLoad || hasItem(hashesToLoad, attachment.metadata?.hash))
);
await this._db.fs.queueDownloads(
@@ -366,7 +367,11 @@ export default class Attachments extends Collection {
async cleanup() {
const now = dayjs().unix();
for (const attachment of this.deleted) {
if (dayjs(attachment.dateDeleted).add(7, "days").unix() < now) continue;
if (
!attachment.metadata ||
dayjs(attachment.dateDeleted).add(7, "days").unix() < now
)
continue;
const isDeleted = await this._db.fs.deleteFile(attachment.metadata.hash);
if (!isDeleted) continue;
@@ -377,7 +382,9 @@ export default class Attachments extends Collection {
get pending() {
return this.all.filter(
(attachment) => attachment.dateUploaded <= 0 || !attachment.dateUploaded
(attachment) =>
attachment.metadata &&
(attachment.dateUploaded <= 0 || !attachment.dateUploaded)
);
}
@@ -398,23 +405,30 @@ export default class Attachments extends Collection {
}
get images() {
return this.all.filter((attachment) => isImage(attachment.metadata.type));
return this.all.filter(
(attachment) => attachment.metadata && isImage(attachment.metadata.type)
);
}
get webclips() {
return this.all.filter((attachment) => isWebClip(attachment.metadata.type));
return this.all.filter(
(attachment) => attachment.metadata && isWebClip(attachment.metadata.type)
);
}
get media() {
return this.all.filter(
(attachment) =>
isImage(attachment.metadata.type) || isWebClip(attachment.metadata.type)
attachment.metadata &&
(isImage(attachment.metadata.type) ||
isWebClip(attachment.metadata.type))
);
}
get files() {
return this.all.filter(
(attachment) =>
attachment.metadata &&
!isImage(attachment.metadata.type) &&
!isWebClip(attachment.metadata.type)
);

View File

@@ -122,7 +122,9 @@ export default class Content extends Collection {
const content = getContentFromData(contentItem.type, contentItem.data);
if (!content) console.log(contentItem);
contentItem.data = await content.insertMedia(async (hashes) => {
const attachments = hashes.map((h) => this._db.attachments.attachment(h));
const attachments = hashes
.map((h) => this._db.attachments.attachment(h))
.filter((a) => !!a && !!a.metadata);
await this._db.fs.queueDownloads(
attachments.map((a) => ({
filename: a.metadata.hash,
@@ -134,6 +136,8 @@ export default class Content extends Collection {
);
const sources = {};
for (const attachment of attachments) {
if (!attachment.metadata) continue;
const src = await this._db.attachments.read(
attachment.metadata.hash,
getOutputType(attachment)
@@ -188,20 +192,23 @@ export default class Content extends Collection {
const toDelete = noteAttachments.filter((attachment) => {
return attachments.every(
(a) => a.hash && a.hash !== attachment.metadata.hash
(a) => a && a.hash && a.hash !== attachment.metadata?.hash
);
});
const toAdd = attachments.filter((attachment) => {
return (
attachment &&
attachment.hash &&
noteAttachments.every((a) => attachment.hash !== a.metadata.hash)
noteAttachments.every((a) => attachment.hash !== a.metadata?.hash)
);
});
for (let attachment of toDelete) {
if (!attachment.metadata) continue;
await this._db.attachments.delete(
attachment.metadata.hash,
attachment.metadata?.hash,
contentItem.noteId
);
}

View File

@@ -251,6 +251,7 @@ export default class Notes extends Collection {
const attachments = this._db.attachments.ofNote(itemData.id, "all");
for (let attachment of attachments) {
if (!attachment || !attachment.metadata) continue;
await this._db.attachments.delete(
attachment.metadata.hash,
itemData.id

View File

@@ -200,7 +200,7 @@ export function isReminderToday(reminder) {
/**
* @param {Reminder} reminder
*/
function getUpcomingReminderTime(reminder) {
export function getUpcomingReminderTime(reminder) {
if (reminder.mode === "once") return reminder.date;
// this is only the time (hour & minutes); date is not included
const time = dayjs(reminder.date);

View File

@@ -149,8 +149,9 @@ exports[`convert HTML to markdown with outlinelists > html-to-md-outlinelists.md
`;
exports[`convert HTML to markdown with singleSpacedParagraphs > html-to-md-singleSpacedParagraphs.md 1`] = `
"hello world
"hello world
hello world 2
"
`;

View File

@@ -177,8 +177,10 @@ export default class Backup {
/**
*
* @param {any} backup the backup data
* @param {string} [password]
* @param {string} [key]
*/
async import(backup, password) {
async import(backup, password, key) {
if (!backup) return;
if (!this._validate(backup)) throw new Error("Invalid backup.");
@@ -188,12 +190,14 @@ export default class Backup {
let db = backup.data;
const isEncrypted = db.salt && db.iv && db.cipher;
if (backup.encrypted || isEncrypted) {
if (!password)
if (!password && !key)
throw new Error(
"Please provide a password to decrypt this backup & restore it."
"Please provide a password or an encryption key to decrypt this backup & restore it."
);
const key = await this._db.storage.generateCryptoKey(password, db.salt);
key = key
? { key, salt: db.salt }
: await this._db.storage.generateCryptoKey(password, db.salt);
if (!key)
throw new Error("Could not generate encryption key for backup.");

View File

@@ -22,7 +22,7 @@ const _ignore = "";
/**
* @typedef {{
* groupBy: "abc" | "year" | "month" | "week" | "none" | undefined,
* sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "dateModified" | "title",
* sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "dateModified" | "title" | "dueDate",
* sortDirection: "desc" | "asc"
* }} GroupOptions
*/

View File

@@ -17,7 +17,10 @@ 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 { isReminderActive } from "../collections/reminders";
import {
getUpcomingReminderTime,
isReminderActive
} from "../collections/reminders";
import "../types";
import { getWeekGroupFromTimestamp, MONTHS_FULL } from "./date";
@@ -41,6 +44,10 @@ const comparators = {
asc: (a, b) => a.dateDeleted - b.dateDeleted,
desc: (a, b) => b.dateDeleted - a.dateDeleted
},
dueDate: {
asc: (a, b) => getUpcomingReminderTime(a) - getUpcomingReminderTime(b),
desc: (a, b) => getUpcomingReminderTime(b) - getUpcomingReminderTime(a)
},
title: {
asc: (a, b) =>
getTitle(a).localeCompare(getTitle(b), undefined, { numeric: true }),
@@ -140,12 +147,23 @@ export function groupArray(
* @param {GroupOptions} options
* @returns {(Reminder | {type: "header", title: string})[]} Grouped array
*/
export function groupReminders(array) {
export function groupReminders(
array,
options = {
sortBy: "dateEdited",
sortDirection: "desc"
}
) {
const groups = new Map([
["Active", []],
["Inactive", []]
]);
if (options.sortBy && options.sortDirection) {
const selector = comparators[options.sortBy][options.sortDirection];
array.sort(selector);
}
array.forEach((item) => {
const groupTitle = isReminderActive(item) ? "Active" : "Inactive";
addToGroup(groups, groupTitle, item);

View File

@@ -100,9 +100,16 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
corsHost: settings.corsProxy
},
dateFormat: settings.dateFormat,
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined,
enableInputRules: settings.markdownShortcuts
},
[layout, settings.readonly, tick, settings.doubleSpacedLines]
[
layout,
settings.readonly,
tick,
settings.doubleSpacedLines,
settings.markdownShortcuts
]
);
const update = useCallback(() => {

Some files were not shown because too many files have changed in this diff Show More