mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
3 Commits
3.0.16-bet
...
fix-github
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e68dd129e | ||
|
|
2350d0e41d | ||
|
|
fbd5c8e3cd |
26
.github/workflows/android.publish.yml
vendored
26
.github/workflows/android.publish.yml
vendored
@@ -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
|
||||
|
||||
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() }))
|
||||
|
||||
@@ -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
|
||||
);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
|
||||
|
||||
@@ -41,8 +41,7 @@ export const config = {
|
||||
isSpellCheckerEnabled: true,
|
||||
zoomFactor: 1,
|
||||
theme: nativeTheme.themeSource,
|
||||
automaticUpdates: true,
|
||||
proxyRules: ""
|
||||
automaticUpdates: true
|
||||
};
|
||||
|
||||
type ConfigKey = keyof typeof config;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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") ||
|
||||
|
||||
@@ -481,7 +481,7 @@ export const useAppEvents = () => {
|
||||
|
||||
subscribeToIAPListeners();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(error);
|
||||
DatabaseLogger.error(e);
|
||||
|
||||
ToastEvent.error(e, "An error occurred", "global");
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ export type Settings = {
|
||||
dateFormat: string;
|
||||
timeFormat: string;
|
||||
fontScale: number;
|
||||
markdownShortcuts: boolean;
|
||||
};
|
||||
|
||||
export type EditorProps = {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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: [] })
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -36,7 +36,7 @@ function confirmDeleteAllNotes(items, type, context) {
|
||||
}?`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: (_inputValue, value) => {
|
||||
positivePress: (value) => {
|
||||
setTimeout(() => {
|
||||
resolve({ delete: true, deleteNotes: value });
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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!
|
||||
@@ -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)";
|
||||
|
||||
15
apps/mobile/package-lock.json
generated
15
apps/mobile/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.18",
|
||||
"version": "2.6.16",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
2
apps/theme-builder/package-lock.json
generated
2
apps/theme-builder/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
Binary file not shown.
12
apps/web/package-lock.json
generated
12
apps/web/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
5
apps/web/src/global.d.ts
vendored
5
apps/web/src/global.d.ts
vendored
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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!
|
||||
@@ -1,3 +0,0 @@
|
||||
- Minor bug fixes
|
||||
|
||||
Thank you for using Notesnook!
|
||||
11
packages/common/package-lock.json
generated
11
packages/common/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
61
packages/core/package-lock.json
generated
61
packages/core/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
57
packages/core/src/api/outbox.js
Normal file
57
packages/core/src/api/outbox.js
Normal 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;
|
||||
@@ -99,10 +99,8 @@ class Settings {
|
||||
? "dateDeleted"
|
||||
: key === "tags"
|
||||
? "dateCreated"
|
||||
: key === "reminders"
|
||||
? "dueDate"
|
||||
: "dateEdited",
|
||||
sortDirection: key === "reminders" ? "asc" : "desc"
|
||||
sortDirection: "desc"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -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.");
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user