Compare commits

..

3 Commits

Author SHA1 Message Date
Ammar Ahmed
5e68dd129e Update android.publish.yml
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2024-02-27 01:03:48 +05:00
Ammar Ahmed
2350d0e41d mobile: minor fix 2024-02-27 01:02:39 +05:00
Ammar Ahmed
fbd5c8e3cd mobile: fix github release 2024-02-27 01:01:07 +05:00
111 changed files with 581 additions and 1430 deletions

View File

@@ -28,32 +28,6 @@ jobs:
- name: Make Gradlew Executable
run: cd apps/mobile/native/android && chmod +x ./gradlew
- name: Build unsigned app bundle
run: yarn release:android:bundle
- name: Sign app bundle for Playstore release
id: sign_app
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: apps/mobile/native/android/app/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "33.0.0"
- name: Publish to Playstore
id: deploy
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.streetwriters.notesnook
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
whatsNewDirectory: apps/mobile/native/android/releasenotes/
- name: Build apks for Github release
run: yarn release:android

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.6.17",
"version": "2.6.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.17",
"version": "2.6.15",
"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.17",
"version": "2.6.15",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",

View File

@@ -54,14 +54,6 @@ 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,13 +118,5 @@ 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,7 +112,6 @@ 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,11 +25,7 @@ Type=Application
Version=${app.getVersion()}
Name=${app.getName()}
Comment=${app.getName()} startup script
Exec=${
process.env.APPIMAGE
? `${process.env.APPIMAGE}${hidden ? " --hidden" : ""}`
: `${process.execPath}${hidden ? " --hidden" : ""}`
}
Exec=${process.execPath}${hidden ? " --hidden" : ""}
StartupNotify=false
Terminal=false`;

View File

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

View File

@@ -131,17 +131,6 @@ 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?.id
? db.attachments.ofNote(note?.id, "all")
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
attachmentSearchValue.current = text;
@@ -137,11 +137,10 @@ export const AttachmentDialog = ({ note }) => {
];
const filterAttachments = (type, _attachments) => {
const attachments = _attachments
? _attachments
: note
? db.attachments.ofNote(note?.id, "all")
: [...(db.attachments.all || [])];
const attachments =
_attachments || note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
switch (type) {
case "all":

View File

@@ -26,15 +26,12 @@ import {
ToastEvent
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
import { eCloseSheet } 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();
@@ -67,9 +64,6 @@ 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({
@@ -79,8 +73,6 @@ export const ChangePassword = () => {
});
setLoading(false);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
ToastEvent.show({
@@ -100,7 +92,6 @@ export const ChangePassword = () => {
padding: 12
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader
title="Change password"
paragraph="Enter your old and new passwords"
@@ -138,14 +129,7 @@ export const ChangePassword = () => {
/>
<Notice
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.`}
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."
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 { View } from "react-native";
import { Modal, View } from "react-native";
import { db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import BiometricService from "../../services/biometrics";
@@ -37,7 +37,6 @@ 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";
@@ -47,6 +46,7 @@ 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,9 +69,6 @@ export const SessionExpired = () => {
eSendEvent(eUserLoggedIn, true);
setVisible(false);
setFocused(false);
useUserStore.setState({
disableAppLockRequests: false
});
},
true
);
@@ -87,9 +84,6 @@ export const SessionExpired = () => {
MMKV.clearStore();
clearAllStores();
setVisible(false);
useUserStore.setState({
disableAppLockRequests: false
});
} catch (e) {
ToastEvent.show({
heading: e.message,
@@ -133,9 +127,6 @@ export const SessionExpired = () => {
email.current = user.email;
setFocused(false);
setVisible(true);
useUserStore.setState({
disableAppLockRequests: true
});
}
}, [email]);
@@ -151,9 +142,6 @@ export const SessionExpired = () => {
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
useUserStore.setState({
disableAppLockRequests: true
});
}}
enableSheetKeyboardHandler={true}
visible={true}

View File

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

View File

@@ -17,19 +17,17 @@ 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 from "react-native-document-picker";
import DocumentPicker, {
DocumentPickerResponse
} 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,
@@ -37,6 +35,7 @@ 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";
@@ -47,6 +46,9 @@ 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);
@@ -182,7 +184,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
title: "Encrypted backup",
input: true,
inputPlaceholder: "Password",
paragraph: "Please enter password of this backup file",
paragraph: "Please enter password of this backup file to restore it",
positiveText: "Restore",
secureTextEntry: true,
onClose: () => {
@@ -190,17 +192,10 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
resolve(undefined);
},
negativeText: "Cancel",
positivePress: async (password, isEncryptionKey) => {
resolve({
value: password,
isEncryptionKey
});
positivePress: async (password) => {
resolve(password);
resolved = true;
return true;
},
check: {
info: "Use encryption key",
type: "transparent"
}
});
});
@@ -240,8 +235,8 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}
};
const restoreBackup = async (backup, password, key) => {
await db.backup.import(backup, password, key);
const restoreBackup = async (backup, password) => {
await db.backup.import(backup, password);
await db.initCollections();
initialize();
ToastEvent.show({
@@ -284,7 +279,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}
let password;
let key;
console.log(`Found ${backupFiles?.length} files to restore from backup`);
for (const path of backupFiles) {
@@ -295,16 +289,10 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
if (parsed.encrypted && !password) {
console.log("Backup is encrypted...", "requesting password");
const { value, isEncryptionKey } = await withPassword();
if (isEncryptionKey) {
key = value;
} else {
password = value;
}
if (!password && !key) throw new Error("Failed to decrypt backup");
password = await withPassword();
if (!password) throw new Error("Failed to decrypt backup");
}
await db.backup.import(parsed, password, key);
await db.backup.import(parsed, password);
console.log("Imported", path);
}
// Remove files from cache
@@ -335,19 +323,10 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
async function restoreFromNNBackup(backup) {
try {
if (backup.data.iv && backup.data.salt) {
const { value, isEncryptionKey } = await withPassword();
let key;
let password;
if (isEncryptionKey) {
key = value;
} else {
password = value;
}
if (key || password) {
const password = await withPassword();
if (password) {
try {
await restoreBackup(backup, password, key);
await restoreBackup(backup, password);
close();
setRestoring(false);
} catch (e) {

View File

@@ -88,14 +88,10 @@ 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={
@@ -146,7 +142,6 @@ 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

@@ -481,7 +481,7 @@ export const useAppEvents = () => {
subscribeToIAPListeners();
} catch (e) {
DatabaseLogger.error(error);
DatabaseLogger.error(e);
ToastEvent.error(e, "An error occurred", "global");
}

View File

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

View File

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

View File

@@ -584,13 +584,6 @@ 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,12 +213,10 @@ async function run(progress, context) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
updateNextBackupTime();
let showBackupCompleteSheet =
progress && SettingsService.get().showBackupCompleteSheet;
let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
if (context) return path;
await sleep(300);
if (showBackupCompleteSheet) {
presentBackupCompleteSheet(backupFilePath);
} else {
@@ -235,7 +233,7 @@ async function run(progress, context) {
return path;
} catch (e) {
await sleep(300);
progress && eSendEvent(eCloseSheet);
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({ type: "send", force: false });
await db.sync(false, 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 = "";
const html = text.replace(/\n/g, "<br />");
let html = text.replace(/\n/g, "<br />");
Notifications.displayNotification({
title: note.title,
message: note.headline || text,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -111,7 +111,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 2090
versionCode 2076
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:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -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,3 +1,10 @@
- Minor bug fixes
- 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
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 = 2090;
CURRENT_PROJECT_VERSION = 2067;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1071,7 +1071,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.18;
MARKETING_VERSION = 2.6.15;
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 = 2090;
CURRENT_PROJECT_VERSION = 2067;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1176,7 +1176,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.18;
MARKETING_VERSION = 2.6.15;
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 = 2090;
CURRENT_PROJECT_VERSION = 2067;
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.18;
MARKETING_VERSION = 2.6.15;
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 = 2090;
CURRENT_PROJECT_VERSION = 2067;
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.18;
MARKETING_VERSION = 2.6.15;
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 = 2090;
CURRENT_PROJECT_VERSION = 2067;
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.18;
MARKETING_VERSION = 2.6.15;
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 = 2090;
CURRENT_PROJECT_VERSION = 2067;
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.18;
MARKETING_VERSION = 2.6.15;
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.17",
"version": "2.6.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "2.6.17",
"version": "2.6.15",
"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": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -26837,11 +26837,12 @@
"@notesnook/core": {
"version": "file:../../packages/core",
"requires": {
"@microsoft/signalr": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/crypto": "file:../crypto",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-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.18",
"version": "2.6.16",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

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

View File

@@ -1377,7 +1377,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "2.6.17",
"version": "2.6.15",
"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-at" content=")(.+?)"/, '$1xxx"')
.replace(/(name="updated-at" content=")(.+?)"/, '$1xxx"');
.replace(/(name="created-on" content=")(.+?)"/, '$1xxx"')
.replace(/(name="last-edited-on" 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="xxx" />
<meta name="updated-at" content="xxx" />
<meta name="created-at" content="22-12-2023 11:33 AM" />
<meta name="updated-at" content="22-12-2023 11:33 AM" />

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "2.6.17",
"version": "2.6.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "2.6.17",
"version": "2.6.15",
"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": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-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.17",
"version": "2.6.15",
"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.17",
"version": "2.6.15",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

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

View File

@@ -94,21 +94,24 @@ class Vault {
}
static lockNote(id) {
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;
}
});
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));
});
}
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 { debounce, getFormattedDate } from "@notesnook/common";
import { getFormattedDate } from "@notesnook/common";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
@@ -445,38 +445,6 @@ 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}</>;
@@ -501,12 +469,10 @@ function EditorChrome(
<Toolbar />
<FlexScrollContainer
scrollRef={editorScrollRef}
className="editorScroll"
style={{ display: "flex", flexDirection: "column", flex: 1 }}
>
<Flex
ref={editorContainerRef}
variant="columnFill"
className="editor"
sx={{
@@ -515,7 +481,7 @@ function EditorChrome(
width: "100%"
}}
pl={6}
pr={6}
pr={2}
onClick={onRequestFocus}
>
{!isMobile && (

View File

@@ -141,9 +141,6 @@ 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();
@@ -183,7 +180,6 @@ function TipTap(props: TipTapProps) {
}
}
},
enableInputRules: markdownShortcuts,
downloadOptions,
doubleSpacedLines,
dateFormat,
@@ -294,14 +290,7 @@ function TipTap(props: TipTapProps) {
},
getAttachmentData: onGetAttachmentData
};
}, [
readonly,
nonce,
doubleSpacedLines,
dateFormat,
timeFormat,
markdownShortcuts
]);
}, [readonly, nonce, doubleSpacedLines, dateFormat, timeFormat]);
const editor = useTiptap(
tiptapOptions,

View File

@@ -56,27 +56,22 @@ type GroupingMenuOptions = {
isUngrouped: boolean;
};
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 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 orderByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
type: "button",
@@ -95,20 +90,12 @@ const orderByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
{
key: "asc",
title:
options.groupOptions.sortBy === "title"
? "A - Z"
: options.groupOptions.sortBy === "dueDate"
? "Earliest first"
: "Oldest - newest"
options.groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
},
{
key: "desc",
title:
options.groupOptions.sortBy === "title"
? "Z - A"
: options.groupOptions.sortBy === "dueDate"
? "Latest first"
: "Newest - oldest"
options.groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
}
])
}
@@ -143,11 +130,6 @@ 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",
@@ -349,26 +331,26 @@ function GroupHeader(props: GroupHeaderProps) {
isUngrouped: false,
refresh
};
const groupBy = groupByMenu({
...menuOptions,
parentKey: "groupBy"
});
const menuItems = [
orderByMenu({
...menuOptions,
parentKey: "sortDirection"
}),
sortByMenu({
...menuOptions,
parentKey: "sortBy"
})
];
if (groupBy) menuItems.push(groupBy);
openMenu(menuItems, {
title: groupBy ? "Group & sort" : "Sort"
});
openMenu(
[
orderByMenu({
...menuOptions,
parentKey: "sortDirection"
}),
sortByMenu({
...menuOptions,
parentKey: "sortBy"
}),
groupByMenu({
...menuOptions,
parentKey: "groupBy"
})
],
{
title: "Group & sort"
}
);
}}
/>
)}

View File

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

View File

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

View File

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

View File

@@ -1,115 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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,34 +91,20 @@ const features: Record<FeatureKeys, Feature> = {
]
: [
{
title: "Sort reminders by due date",
title: "Daily trash cleanup interval",
subtitle:
"You can now sort your reminders by due date to quickly see which reminders are upcoming."
"You can now set the trash cleanup interval to Daily in addition to Weekly, Monthly & Yearly."
},
{
title: "Restore backups using encryption key",
title: "Organized bulk exports",
subtitle:
"If you forget your password but still have your recovery key, you can now use that to restore your backups."
"Bulk exports are now automatically sorted/organized into folders based on your Notebook/Topic organization."
},
{
title: "Disable markdown in editor",
title: "New domain for Monographs",
subtitle:
"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."
}
]
: [])
"Monographs will now be published to monogr.ph instead of monograph.notesnook.com. Don't worry, all your published notes will automatically redirect."
}
],
cta: {
title: "Got it",

View File

@@ -52,9 +52,6 @@ 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,
@@ -82,6 +79,5 @@ export const Dialogs = {
EmailChangeDialog,
AddTagsDialog,
SettingsDialog,
ThemeDetailsDialog,
BackupPasswordDialog
ThemeDetailsDialog
};

View File

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

View File

@@ -19,12 +19,11 @@ 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 { createBackup, verifyAccount } from "../../common";
import { verifyAccount } from "../../common";
import {
show2FARecoveryCodesDialog,
showMultifactorDialog,
showPasswordDialog,
showRecoveryKeyDialog
showPasswordDialog
} from "../../common/dialog-controller";
import { db } from "../../common/db";
import { showToast } from "../../utils/toast";
@@ -46,22 +45,19 @@ 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 (
(await db.user?.changePassword(
db.user?.changePassword(
data.oldPassword,
data.newPassword
)) || false
) || false
);
}
);
if (result) {
showToast("success", "Account password changed!");
await showRecoveryKeyDialog();
}
if (result) showToast("success", "Account password changed!");
}
}
]

View File

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

View File

@@ -1,53 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { 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,7 +29,6 @@ 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[] = [
{
@@ -101,7 +100,7 @@ symbols (e.g. 202305261253)`,
key: "double-spacing",
title: "Double spaced paragraphs",
description:
"Use double spacing between paragraphs when you press Enter in the editor.",
"Use double spacing between paragraphs and when you press Enter in the editor.",
onStateChange: (listener) =>
useSettingStore.subscribe((c) => c.doubleSpacedParagraphs, listener),
components: [
@@ -112,27 +111,6 @@ 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()
}
]
}
]
},
@@ -171,16 +149,6 @@ For example:
component: SpellCheckerLanguages
}
]
},
{
key: "custom-dictionay-words",
title: "Custom dictionary words",
components: [
{
type: "custom",
component: DictionaryWords
}
]
}
]
},

View File

@@ -36,8 +36,7 @@ import {
Privacy,
Pro,
ShieldLock,
Sync,
Proxy
Sync
} from "../../components/icons";
import { Perform } from "../../common/dialog-controller";
import NavigationItem from "../../components/navigation-menu/navigation-item";
@@ -66,6 +65,7 @@ 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,30 +144,6 @@ 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,9 +36,4 @@ 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,7 +27,6 @@ class SpellCheckerStore extends BaseStore<SpellCheckerStore> {
languages: Language[] = [];
enabled = true;
enabledLanguages: Language[] = [];
words: string[] = [];
toggleSpellChecker = async () => {
const enabled = this.get().enabled;
@@ -45,19 +44,14 @@ 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(),
words: (await desktop?.spellChecker.words.query()) || []
enabled: await desktop?.spellChecker.isEnabled.query()
});
};
deleteWord = async (word: string) => {
await desktop?.spellChecker.deleteWord.mutate(word);
await this.get().refresh();
};
}
const [useSpellChecker] = createStore(SpellCheckerStore);

View File

@@ -446,19 +446,18 @@ 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,7 +24,6 @@ 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
@@ -34,9 +33,7 @@ class OriginPrivateFileStore implements IFileStorage {
async clear(): Promise<void> {
for await (const [name] of this.directory) {
await this.safeOp(name, () =>
this.directory.removeEntry(name, { recursive: true })
);
await this.directory.removeEntry(name, { recursive: true });
}
await this.storage.clear();
}
@@ -50,59 +47,33 @@ class OriginPrivateFileStore implements IFileStorage {
return this.storage.delete(filename);
}
async writeChunk(chunkName: string, data: Uint8Array): Promise<void> {
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);
}
const file = await this.directory.getFileHandle(chunkName, {
create: true
});
const syncHandle = await file.createSyncAccessHandle();
syncHandle.write(data);
syncHandle.close();
}
async deleteChunk(chunkName: string) {
try {
await this.safeOp(chunkName, () => this.directory.removeEntry(chunkName));
await this.directory.removeEntry(chunkName);
} catch (e) {
console.error("Failed to delete chunk", e);
}
}
async readChunk(chunkName: string): Promise<Uint8Array | undefined> {
try {
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)
);
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;
} 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();
@@ -146,12 +117,3 @@ 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,11 +298,7 @@ class AppStore extends BaseStore {
this.updateSyncStatus("syncing");
try {
const result = await db.sync({
type: full ? "full" : "send",
force,
serverLastSynced: lastSynced
});
const result = await db.sync(full, force, lastSynced);
if (!result) return this.updateSyncStatus("failed");
this.updateSyncStatus("completed", true);
@@ -311,6 +307,7 @@ 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,8 +72,9 @@ class AttachmentStore extends BaseStore {
await createWriteStream("attachments.zip", {
signal: abortController.signal
})
)
.finally(() => this.set((state) => (state.status = undefined)));
);
this.set((state) => (state.status = undefined));
};
cancel = async () => {

View File

@@ -290,14 +290,12 @@ class EditorStore extends BaseStore {
state: SESSION_STATES.new
};
});
setTimeout(() => {
noteStore.setSelectedNote(0);
this.toggleProperties(false);
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
appStore.setIsEditorOpen(false);
setDocumentTitle();
}, 100);
noteStore.setSelectedNote(0);
this.toggleProperties(false);
if (shouldNavigate)
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
setTimeout(() => appStore.setIsEditorOpen(false), 100);
setDocumentTitle();
};
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.clearSession(true);
await editorStore.openSession(id);
this.refreshItem(id);
return res;
});

View File

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

View File

@@ -38,7 +38,6 @@ 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;
@@ -59,10 +58,6 @@ class SettingStore extends BaseStore {
desktopIntegrationSettings = undefined;
autoUpdates = true;
isFlatpak = false;
/**
* @type {string|undefined}
*/
proxyRules = undefined;
refresh = async () => {
this.set({
@@ -75,8 +70,7 @@ 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(),
proxyRules: await desktop?.integration.proxyRules.query()
autoUpdates: await desktop?.updater.autoUpdates.query()
});
};
@@ -105,11 +99,6 @@ 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);
@@ -167,14 +156,6 @@ 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,7 +26,6 @@ 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") {
@@ -37,23 +36,19 @@ 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) {
const log = allLogs[i++];
if (!log) {
controller.close();
return;
for (const log of allLogs) {
controller.enqueue({
path: log.key,
data: textEncoder.encode(
(log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
)
});
}
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,51 +40,46 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
super({
start() {},
async pull(controller) {
try {
if (signal?.aborted) {
controller.close();
return;
}
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."));
}
} catch (e) {
console.error(e);
controller.error(e);
} finally {
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."));
}
if (index === attachments.length) {
controller.close();
}
}
});

View File

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

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({ type: "fetch", force: true });
await db.sync(true, true);
navigate("backup");
}}
>
@@ -507,7 +507,7 @@ function NewPassword(props: BaseRecoveryComponentProps<"new">) {
if (formData?.backupFile) {
await restoreBackupFile(formData?.backupFile);
await db.sync({ type: "full", force: true });
await db.sync(true, true);
}
navigate("final");

View File

@@ -1,7 +0,0 @@
- 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

@@ -1,3 +0,0 @@
- 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": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -144,11 +144,12 @@
"@notesnook/core": {
"version": "file:../core",
"requires": {
"@microsoft/signalr": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/crypto": "file:../crypto",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-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({ type: "full" });
await deviceB.sync(true);
expect(handler).not.toHaveBeenCalled();
@@ -415,7 +415,7 @@ async function initializeDevice(id, capabilities = []) {
await device.user.resetUser(false);
await device.sync({ type: "full" });
await device.sync(true, false);
console.timeEnd(`Init ${id}`);
return device;
@@ -448,13 +448,7 @@ function syncAndWait(deviceA, deviceB, force = false) {
(full, force, lastSynced) => {
console.log("sync requested by device A", full, force, lastSynced);
ref2.unsubscribe();
deviceB
.sync({
type: full ? "full" : "send",
force,
serverLastSynced: lastSynced
})
.catch(reject);
deviceB.sync(full, force, lastSynced).catch(reject);
}
);
@@ -472,6 +466,6 @@ function syncAndWait(deviceA, deviceB, force = false) {
deviceB.syncer.sync.syncing
);
deviceA.sync({ type: "full", force }).catch(reject);
deviceA.sync(true, force).catch(reject);
});
}

View File

@@ -17,9 +17,8 @@ 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 () => {
@@ -51,55 +50,7 @@ test(
await login(db);
const userData = await db.user.getUser();
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();
expect(userData.email).toBe(user.email);
}),
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, user = USER) {
export async function login(db) {
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": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -494,9 +494,9 @@
}
},
"node_modules/@microsoft/signalr": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.0.tgz",
"integrity": "sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==",
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.10.tgz",
"integrity": "sha512-tOEn32i5EatAx4sZbzmLgcBc2VbKQmx+F4rI2/Ioq2MnBaYcFxbDzOoZgISIS4IR9H1ij/sKoU8zQOAFC8GJKg==",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
@@ -505,6 +505,23 @@
"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
@@ -581,9 +598,9 @@
}
},
"node_modules/@streetwriters/showdown": {
"version": "3.0.5-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.5-alpha.tgz",
"integrity": "sha512-jD9JFhxLDx6XeyZOLVB0zWtwGduwNiFpxn5rxu6ThyKyWGnu1O+L1w04WLC1L56pyEhypr3Tsk24dzo2Se/50g==",
"version": "3.0.4-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.4-alpha.tgz",
"integrity": "sha512-R2UJzMXyJz312RxekXGQIQvLsdZ0eSa4Z7dRXzf/fL2XsPPtyEzkZce0RoZn/mmKfDym5XKnjcVxnF3hXgjVaw==",
"bin": {
"showdown": "bin/showdown.js"
},
@@ -3165,9 +3182,9 @@
}
},
"@microsoft/signalr": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.0.tgz",
"integrity": "sha512-K/wS/VmzRWePCGqGh8MU8OWbS1Zvu7DG7LSJS62fBB8rJUXwwj4axQtqrAAwKGUZHQF6CuteuQR9xMsVpM2JNA==",
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-7.0.10.tgz",
"integrity": "sha512-tOEn32i5EatAx4sZbzmLgcBc2VbKQmx+F4rI2/Ioq2MnBaYcFxbDzOoZgISIS4IR9H1ij/sKoU8zQOAFC8GJKg==",
"requires": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
@@ -3176,6 +3193,20 @@
"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": {
@@ -3247,9 +3278,9 @@
}
},
"@streetwriters/showdown": {
"version": "3.0.5-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.5-alpha.tgz",
"integrity": "sha512-jD9JFhxLDx6XeyZOLVB0zWtwGduwNiFpxn5rxu6ThyKyWGnu1O+L1w04WLC1L56pyEhypr3Tsk24dzo2Se/50g=="
"version": "3.0.4-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.4-alpha.tgz",
"integrity": "sha512-R2UJzMXyJz312RxekXGQIQvLsdZ0eSa4Z7dRXzf/fL2XsPPtyEzkZce0RoZn/mmKfDym5XKnjcVxnF3hXgjVaw=="
},
"@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",
"postinstall": "patch-package"
"test": "vitest run"
},
"dependencies": {
"@microsoft/signalr": "^8.0.0",
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@readme/data-urls": "^3.0.0",
"@streetwriters/showdown": "^3.0.5-alpha",
"@streetwriters/showdown": "^3.0.4-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",

View File

@@ -1,52 +0,0 @@
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,6 +33,7 @@ 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";
@@ -89,6 +90,7 @@ 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();
@@ -113,7 +115,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();
@@ -125,6 +127,7 @@ class Database {
await this.initCollections();
await this.outbox.init();
await this.migrations.init();
this.isInitialized = true;
if (this.migrations.required()) {
@@ -272,17 +275,8 @@ class Database {
return (await this.storage.read("lastSynced")) || 0;
}
/**
*
* @param {{
* type: "full" | "fetch" | "send";
* force?: boolean;
* serverLastSynced?: number;
* }} options
* @returns
*/
sync(options) {
return this.syncer.start(options);
sync(full = true, force = false, lastSynced = null) {
return this.syncer.start(full, force, lastSynced);
}
/**

View File

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

View File

@@ -0,0 +1,57 @@
/*
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,10 +99,8 @@ class Settings {
? "dateDeleted"
: key === "tags"
? "dateCreated"
: key === "reminders"
? "dueDate"
: "dateEdited",
sortDirection: key === "reminders" ? "asc" : "desc"
sortDirection: "desc"
}
);
}

View File

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

View File

@@ -165,17 +165,10 @@ 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.metadata &&
localItem.dateUploaded !== remoteItem.dateUploaded
) {
if (localItem && localItem.dateUploaded !== remoteItem.dateUploaded) {
const noteIds = localItem.noteIds.slice();
const isRemoved = await this._db.attachments.remove(
localItem.metadata.hash,

View File

@@ -129,43 +129,39 @@ class UserManager {
) {
if (!email || !password) throw new Error("email & password are required.");
const token = await this.tokenManager.getToken();
if (!token) throw new Error("No token found.");
const token = await this.tokenManager.getAccessToken();
if (!token) throw new Error("Unauthorized.");
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
)
);
const user = await this.fetchUser();
if (!user) throw new Error("Unauthorized.");
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
)
);
if (!sessionExpired) {
await this._storage.write("lastSynced", 0);
}
const user = await this.fetchUser();
if (!user) throw new Error("Unauthorized.");
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;
await this._storage.deriveCryptoKey(`_uk_@${user.email}`, {
password,
salt: user.salt
});
if (!sessionExpired) {
await this._storage.write("lastSynced", 0);
}
EV.publish(EVENTS.userLoggedIn, user);
}
/**
@@ -458,44 +454,43 @@ 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.clearSessions();
await this._storage.deriveCryptoKey(`_uk_@${email}`, {
password: new_password,
salt
});
if (data.encryptionKey) await this._db.sync({ type: "fetch", force: true });
if (!(await this.resetUser(false))) return;
await this._storage.deriveCryptoKey(`_uk_@${email}`, {
password: new_password,
salt
});
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 (!(await this.resetUser(false))) return;
await this._db.sync(false, true);
await this._db.sync({ type: "send", force: 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);
if (attachmentsKey) {
const userEncryptionKey = await this.getEncryptionKey();
if (!userEncryptionKey) return;
user.attachmentsKey = await this._storage.encrypt(
userEncryptionKey,
JSON.stringify(attachmentsKey)
await http.patch(
`${constants.AUTH_HOST}${ENDPOINTS.patchUser}`,
{
type,
old_password,
new_password
},
token
);
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 || !attachment.metadata) return;
if (!attachment) 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 || !attachment.metadata) return false;
if (!attachment) 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,9 +348,8 @@ 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(
@@ -367,11 +366,7 @@ export default class Attachments extends Collection {
async cleanup() {
const now = dayjs().unix();
for (const attachment of this.deleted) {
if (
!attachment.metadata ||
dayjs(attachment.dateDeleted).add(7, "days").unix() < now
)
continue;
if (dayjs(attachment.dateDeleted).add(7, "days").unix() < now) continue;
const isDeleted = await this._db.fs.deleteFile(attachment.metadata.hash);
if (!isDeleted) continue;
@@ -382,9 +377,7 @@ export default class Attachments extends Collection {
get pending() {
return this.all.filter(
(attachment) =>
attachment.metadata &&
(attachment.dateUploaded <= 0 || !attachment.dateUploaded)
(attachment) => attachment.dateUploaded <= 0 || !attachment.dateUploaded
);
}
@@ -405,30 +398,23 @@ export default class Attachments extends Collection {
}
get images() {
return this.all.filter(
(attachment) => attachment.metadata && isImage(attachment.metadata.type)
);
return this.all.filter((attachment) => isImage(attachment.metadata.type));
}
get webclips() {
return this.all.filter(
(attachment) => attachment.metadata && isWebClip(attachment.metadata.type)
);
return this.all.filter((attachment) => isWebClip(attachment.metadata.type));
}
get media() {
return this.all.filter(
(attachment) =>
attachment.metadata &&
(isImage(attachment.metadata.type) ||
isWebClip(attachment.metadata.type))
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,9 +122,7 @@ 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))
.filter((a) => !!a && !!a.metadata);
const attachments = hashes.map((h) => this._db.attachments.attachment(h));
await this._db.fs.queueDownloads(
attachments.map((a) => ({
filename: a.metadata.hash,
@@ -136,8 +134,6 @@ 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)
@@ -192,23 +188,20 @@ export default class Content extends Collection {
const toDelete = noteAttachments.filter((attachment) => {
return attachments.every(
(a) => a && a.hash && a.hash !== attachment.metadata?.hash
(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,7 +251,6 @@ 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
*/
export function getUpcomingReminderTime(reminder) {
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,9 +149,8 @@ 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,10 +177,8 @@ export default class Backup {
/**
*
* @param {any} backup the backup data
* @param {string} [password]
* @param {string} [key]
*/
async import(backup, password, key) {
async import(backup, password) {
if (!backup) return;
if (!this._validate(backup)) throw new Error("Invalid backup.");
@@ -190,14 +188,12 @@ export default class Backup {
let db = backup.data;
const isEncrypted = db.salt && db.iv && db.cipher;
if (backup.encrypted || isEncrypted) {
if (!password && !key)
if (!password)
throw new Error(
"Please provide a password or an encryption key to decrypt this backup & restore it."
"Please provide a password to decrypt this backup & restore it."
);
key = key
? { key, salt: db.salt }
: await this._db.storage.generateCryptoKey(password, db.salt);
const key = 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" | "dueDate",
* sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "dateModified" | "title",
* sortDirection: "desc" | "asc"
* }} GroupOptions
*/

View File

@@ -17,10 +17,7 @@ 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 {
getUpcomingReminderTime,
isReminderActive
} from "../collections/reminders";
import { isReminderActive } from "../collections/reminders";
import "../types";
import { getWeekGroupFromTimestamp, MONTHS_FULL } from "./date";
@@ -44,10 +41,6 @@ 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 }),
@@ -147,23 +140,12 @@ export function groupArray(
* @param {GroupOptions} options
* @returns {(Reminder | {type: "header", title: string})[]} Grouped array
*/
export function groupReminders(
array,
options = {
sortBy: "dateEdited",
sortDirection: "desc"
}
) {
export function groupReminders(array) {
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,16 +100,9 @@ const Tiptap = ({ settings }: { settings: Settings }) => {
corsHost: settings.corsProxy
},
dateFormat: settings.dateFormat,
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined,
enableInputRules: settings.markdownShortcuts
timeFormat: settings.timeFormat as "12-hour" | "24-hour" | undefined
},
[
layout,
settings.readonly,
tick,
settings.doubleSpacedLines,
settings.markdownShortcuts
]
[layout, settings.readonly, tick, settings.doubleSpacedLines]
);
const update = useCallback(() => {

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