mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 02:29:18 +02:00
Compare commits
3 Commits
3.0.9-beta
...
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.16",
|
||||
"version": "2.6.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.16",
|
||||
"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.16",
|
||||
"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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -61,7 +61,10 @@ export const getGithubVersion = async () => {
|
||||
if (!res?.ok) return null;
|
||||
const data = (await res?.json()) as GithubRelease[];
|
||||
|
||||
const versions = data?.filter((tag) => tag.tag_name.endsWith("android"));
|
||||
const versions = data?.filter(
|
||||
(tag) =>
|
||||
tag.tag_name.endsWith("android") && !tag.tag_name.endsWith("beta-android")
|
||||
);
|
||||
const latestVersion = versions[0];
|
||||
const version = latestVersion.tag_name.replace("-android", "");
|
||||
return {
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 2084
|
||||
versionCode 2076
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -997,7 +997,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2085;
|
||||
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.17;
|
||||
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 = 2085;
|
||||
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.17;
|
||||
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 = 2085;
|
||||
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.17;
|
||||
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 = 2085;
|
||||
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.17;
|
||||
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 = 2085;
|
||||
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.17;
|
||||
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 = 2085;
|
||||
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.17;
|
||||
MARKETING_VERSION = 2.6.15;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.17",
|
||||
"version": "2.6.16",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -45,4 +45,4 @@
|
||||
"react-native": "0.72.0",
|
||||
"react-native-iap": "12.11.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.14",
|
||||
"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" />
|
||||
|
||||
|
||||
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.6.16",
|
||||
"version": "2.6.15",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "2.6.16",
|
||||
"version": "2.6.15",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@aws-sdk/util-base64-browser": "^3.208.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.16",
|
||||
"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);
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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!
|
||||
@@ -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);
|
||||
|
||||
67
packages/core/package-lock.json
generated
67
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",
|
||||
@@ -4158,8 +4189,7 @@
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4944,8 +4974,7 @@
|
||||
"ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"requires": {}
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q=="
|
||||
},
|
||||
"xml-name-validator": {
|
||||
"version": "4.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();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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,9 +165,6 @@ 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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -52,7 +52,6 @@ export type Settings = {
|
||||
timeFormat: string;
|
||||
dateFormat: string;
|
||||
fontScale: number;
|
||||
markdownShortcuts: boolean;
|
||||
};
|
||||
|
||||
/* eslint-disable no-var */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/@tiptap/core/dist/index.js b/node_modules/@tiptap/core/dist/index.js
|
||||
index 38c9884..4a9e10c 100644
|
||||
index 38c9884..2dad7fe 100644
|
||||
--- a/node_modules/@tiptap/core/dist/index.js
|
||||
+++ b/node_modules/@tiptap/core/dist/index.js
|
||||
@@ -1876,8 +1876,23 @@ const lift = (typeOrName, attributes = {}) => ({ state, dispatch }) => {
|
||||
@@ -1876,8 +1876,21 @@ const lift = (typeOrName, attributes = {}) => ({ state, dispatch }) => {
|
||||
return lift$1(state, dispatch);
|
||||
};
|
||||
|
||||
@@ -15,8 +15,6 @@ index 38c9884..4a9e10c 100644
|
||||
+ const { selection, storedMarks } = state;
|
||||
+ const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks());
|
||||
+
|
||||
+ if (!marks) return dispatch(tr);
|
||||
+
|
||||
+ const { splittableMarks } = editor.extensionManager;
|
||||
+ const filteredMarks = marks.filter((mark) =>
|
||||
+ splittableMarks.includes(mark.type.name)
|
||||
|
||||
@@ -38,7 +38,6 @@ export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||
|
||||
parseSlice(dom: Node, options?: ParseOptions | undefined): Slice {
|
||||
if (dom instanceof HTMLElement || dom instanceof Document) {
|
||||
convertGoogleDocsChecklist(dom);
|
||||
formatCodeblocks(dom);
|
||||
convertBrToSingleSpacedParagraphs(dom);
|
||||
}
|
||||
@@ -76,43 +75,29 @@ export function convertBrToSingleSpacedParagraphs(dom: HTMLElement | Document) {
|
||||
for (const br of dom.querySelectorAll("br")) {
|
||||
let paragraph = br.closest("p");
|
||||
|
||||
if (!paragraph) {
|
||||
// we split and wrap all text nodes into their own single spaced
|
||||
// paragraphs
|
||||
const nodes = getSiblingTextNodes(br);
|
||||
if (nodes.length > 0) {
|
||||
paragraph = document.createElement("p");
|
||||
paragraph.dataset.spacing = "single";
|
||||
paragraph.append(...nodes);
|
||||
br.replaceWith(paragraph);
|
||||
continue;
|
||||
}
|
||||
|
||||
// we convert the next pargraph into a single spaced paragraph
|
||||
if (br.nextElementSibling instanceof HTMLParagraphElement) {
|
||||
br.nextElementSibling.dataset.spacing = "single";
|
||||
}
|
||||
|
||||
// just convert all br tags into single spaced paragraphs
|
||||
const newParagraph = document.createElement("p");
|
||||
newParagraph.dataset.spacing = "single";
|
||||
br.replaceWith(newParagraph);
|
||||
// if no paragraph is found over the br, we add one.
|
||||
if (!paragraph && br.parentElement) {
|
||||
const parent = br.parentElement;
|
||||
const p = document.createElement("p");
|
||||
p.append(...parent.childNodes);
|
||||
parent.append(p);
|
||||
paragraph = p;
|
||||
}
|
||||
|
||||
// if paragraph is empty, we clean out the paragraph and move on.
|
||||
if (
|
||||
paragraph &&
|
||||
(paragraph.childNodes.length === 1 ||
|
||||
!paragraph.textContent ||
|
||||
paragraph.textContent.trim().length === 0)
|
||||
) {
|
||||
// if paragraph is empty, we clean out the paragraph and move on.
|
||||
paragraph.innerHTML = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (paragraph) {
|
||||
splitOn(paragraph, br);
|
||||
const children = Array.from(paragraph.childNodes);
|
||||
const children = Array.from(paragraph.childNodes.values());
|
||||
const newParagraph = document.createElement("p");
|
||||
newParagraph.dataset.spacing = "single";
|
||||
newParagraph.append(...children.slice(children.indexOf(br) + 1));
|
||||
@@ -122,18 +107,6 @@ export function convertBrToSingleSpacedParagraphs(dom: HTMLElement | Document) {
|
||||
}
|
||||
}
|
||||
|
||||
export function convertGoogleDocsChecklist(dom: HTMLElement | Document) {
|
||||
for (const li of dom.querySelectorAll(`ul li[role="checkbox"]`)) {
|
||||
if (!li.parentElement?.classList.contains("checklist"))
|
||||
li.parentElement!.classList.add("checklist");
|
||||
li.className = "checklist--item";
|
||||
if (li.firstElementChild?.tagName === "IMG") li.firstElementChild.remove();
|
||||
if (li.getAttribute("aria-checked") === "true") {
|
||||
li.classList.add("checked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function splitOn(bound: Element, cutElement: Element) {
|
||||
let grandparent: ParentNode | null = null;
|
||||
for (
|
||||
@@ -150,13 +123,3 @@ function splitOn(bound: Element, cutElement: Element) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSiblingTextNodes(element: ChildNode) {
|
||||
const siblings = [];
|
||||
let sibling: ChildNode | null = element;
|
||||
while ((sibling = sibling.previousSibling)) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE) break;
|
||||
else if (sibling.nodeType === Node.TEXT_NODE) siblings.push(sibling);
|
||||
}
|
||||
return siblings;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,14 @@ exports[`convert br tags to paragraphs 4`] = `"<p>line <span><em>1</em></span></
|
||||
exports[`convert br tags to paragraphs 5`] = `"<p></p>"`;
|
||||
|
||||
exports[`convert br tags to paragraphs 6`] = `
|
||||
"<!--StartFragment--><p data-spacing=\\"single\\">A troll, they call me, but I have no wish
|
||||
</p><p data-spacing=\\"single\\">
|
||||
"<p>
|
||||
<!--StartFragment-->A troll, they call me, but I have no wish</p><p data-spacing=\\"single\\">
|
||||
to be associated with those dolls</p><p data-spacing=\\"single\\">
|
||||
</p><p data-spacing=\\"single\\">
|
||||
We lack religion, purpose, politics,</p><p data-spacing=\\"single\\">
|
||||
and yet, we somehow manage to get by.</p>"
|
||||
and yet, we somehow manage to get by.</p><p data-spacing=\\"single\\">
|
||||
|
||||
</p>"
|
||||
`;
|
||||
|
||||
exports[`convert br tags to paragraphs 7`] = `
|
||||
@@ -52,36 +54,6 @@ exports[`convert br tags to paragraphs 9`] = `
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`convert br tags to paragraphs 10`] = `
|
||||
"<div>
|
||||
<p style=\\"line-height: 100%; margin-bottom: 0in\\">
|
||||
I am not talking to you</p><p data-spacing=\\"single\\">
|
||||
</p>
|
||||
<p>I am talking to you</p>
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`convert br tags to paragraphs 11`] = `
|
||||
"<div>
|
||||
<!--StartFragment--><p><span>Hello</span></p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"></p><p data-spacing=\\"single\\"><span>world</span></p><!--EndFragment-->
|
||||
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`convert br tags to paragraphs 12`] = `
|
||||
"<div>
|
||||
<!--StartFragment--><meta charset=\\"utf-8\\"><h1><span>Write notes</span></h1><p data-spacing=\\"single\\"></p><p dir=\\"ltr\\" data-spacing=\\"single\\"><span>Welcome of </span><a><span>Notesnook</span></a><span>, an syncing.</span></p><p data-spacing=\\"single\\"></p><p dir=\\"ltr\\" data-spacing=\\"single\\"><span>Enjoy the read!</span></p><p data-spacing=\\"single\\"></p><p dir=\\"ltr\\" data-spacing=\\"single\\"><span><span><img></span></span></p><p data-spacing=\\"single\\"></p><p dir=\\"ltr\\" data-spacing=\\"single\\"><span>vision?</span></p><p data-spacing=\\"single\\"></p><p dir=\\"ltr\\" data-spacing=\\"single\\"><span>everyone means</span></p><!--EndFragment-->
|
||||
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`convert google docs checklist 1`] = `
|
||||
"<div>
|
||||
<!--StartFragment--><meta charset=\\"utf-8\\"><ul id=\\"docs-internal-guid-0d9a5db3-7fff-ab55-e7ca-b178e1031970\\" class=\\"checklist\\"><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"false\\" aria-level=\\"1\\" class=\\"checklist--item\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Adsjkfhasdf</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"false\\" aria-level=\\"1\\" class=\\"checklist--item\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Asdfsadf</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"true\\" aria-level=\\"1\\" class=\\"checklist--item checked\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Asdfsda</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"true\\" aria-level=\\"1\\" class=\\"checklist--item checked\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Fasd</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"true\\" aria-level=\\"1\\" class=\\"checklist--item checked\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Fasd</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"false\\" aria-level=\\"1\\" class=\\"checklist--item\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>F</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"true\\" aria-level=\\"1\\" class=\\"checklist--item checked\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>akcasb</span></p></li><ul class=\\"checklist\\"><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"true\\" aria-level=\\"2\\" class=\\"checklist--item checked\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Asdf</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"false\\" aria-level=\\"2\\" class=\\"checklist--item\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Asdcasdc</span></p></li><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"false\\" aria-level=\\"2\\" class=\\"checklist--item\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>sdac</span></p></li></ul><li dir=\\"ltr\\" role=\\"checkbox\\" aria-checked=\\"false\\" aria-level=\\"1\\" class=\\"checklist--item\\"><p dir=\\"ltr\\" role=\\"presentation\\"><span>Asdfsda</span></p></li></ul><!--EndFragment-->
|
||||
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`properly format codeblocks 1`] = `
|
||||
"<div>
|
||||
<!--StartFragment--><p>Sure! Here's an implementation of a word counter for Thai that considers each syllable consisting of a consonant sound followed by a vowel sound as a word:</p><pre><code>javascript</code></pre><pre class=\\"language-javascript\\"><code>function countThaiWords(text) {
|
||||
|
||||
@@ -20,8 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
formatCodeblocks,
|
||||
convertBrToSingleSpacedParagraphs,
|
||||
convertGoogleDocsChecklist
|
||||
convertBrToSingleSpacedParagraphs
|
||||
} from "../clipboard-dom-parser";
|
||||
|
||||
const cases = [
|
||||
@@ -78,31 +77,11 @@ and yet, we somehow manage to get by.<br>
|
||||
you</p>
|
||||
|
||||
</div>`
|
||||
],
|
||||
[
|
||||
`<div>
|
||||
<p style="line-height: 100%; margin-bottom: 0in">
|
||||
I am not talking to you</p>
|
||||
<br/>
|
||||
<p>I am talking to you</p>
|
||||
</div>`
|
||||
],
|
||||
[
|
||||
`<div>
|
||||
<!--StartFragment--><p><span>Hello</span></p><br><br><br><br><br><p><span >world</span></p><!--EndFragment-->
|
||||
|
||||
</div>`
|
||||
],
|
||||
[
|
||||
`<div>
|
||||
<!--StartFragment--><meta charset="utf-8"><h1><span >Write notes</span></h1><br><p dir="ltr" ><span >Welcome of </span><a><span >Notesnook</span></a><span >, an syncing.</span></p><br><p dir="ltr" ><span >Enjoy the read!</span></p><br><p dir="ltr" ><span ><span ><img></span></span></p><br><p dir="ltr" ><span >vision?</span></p><br><p dir="ltr" ><span >everyone means</span></p><!--EndFragment-->
|
||||
|
||||
</div>`
|
||||
]
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const [html] = testCase;
|
||||
const [html, expected] = testCase;
|
||||
test(`convert br tags to paragraphs`, (t) => {
|
||||
const element = new DOMParser().parseFromString(html, "text/html");
|
||||
convertBrToSingleSpacedParagraphs(element);
|
||||
@@ -140,17 +119,3 @@ for (const codeBlock of codeBlocks) {
|
||||
t.expect(element.body.innerHTML.trim()).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
|
||||
const checkLists = [
|
||||
`<div>
|
||||
<!--StartFragment--><meta charset="utf-8"><ul id="docs-internal-guid-0d9a5db3-7fff-ab55-e7ca-b178e1031970"><li dir="ltr" role="checkbox" aria-checked="false" aria-level="1"><img alt="unchecked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Adsjkfhasdf</span></p></li><li dir="ltr" role="checkbox" aria-checked="false" aria-level="1"><img alt="unchecked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Asdfsadf</span></p></li><li dir="ltr" role="checkbox" aria-checked="true" aria-level="1"><img alt="checked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Asdfsda</span></p></li><li dir="ltr" role="checkbox" aria-checked="true" aria-level="1"><img alt="checked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Fasd</span></p></li><li dir="ltr" role="checkbox" aria-checked="true" aria-level="1"><img alt="checked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Fasd</span></p></li><li dir="ltr" role="checkbox" aria-checked="false" aria-level="1"><img alt="unchecked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >F</span></p></li><li dir="ltr" role="checkbox" aria-checked="true" aria-level="1"><img alt="checked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >akcasb</span></p></li><ul ><li dir="ltr" role="checkbox" aria-checked="true" aria-level="2"><img alt="checked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Asdf</span></p></li><li dir="ltr" role="checkbox" aria-checked="false" aria-level="2"><img alt="unchecked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Asdcasdc</span></p></li><li dir="ltr" role="checkbox" aria-checked="false" aria-level="2"><img alt="unchecked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >sdac</span></p></li></ul><li dir="ltr" role="checkbox" aria-checked="false" aria-level="1"><img alt="unchecked" aria-roledescription="checkbox" ><p dir="ltr" role="presentation"><span >Asdfsda</span></p></li></ul><!--EndFragment-->
|
||||
|
||||
</div>`
|
||||
];
|
||||
for (const checkList of checkLists) {
|
||||
test(`convert google docs checklist`, (t) => {
|
||||
const element = new DOMParser().parseFromString(checkList, "text/html");
|
||||
convertGoogleDocsChecklist(element);
|
||||
t.expect(element.body.innerHTML.trim()).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -230,9 +230,7 @@ function TableColumnToolbar(props: TableToolbarProps) {
|
||||
yOffset: 2
|
||||
});
|
||||
|
||||
columnToolsRef.current.style.left = `${
|
||||
pos.left - (table.current.parentElement?.scrollLeft || 0)
|
||||
}px`;
|
||||
columnToolsRef.current.style.left = `${pos.left}px`;
|
||||
columnToolsRef.current.style.top = `${pos.top}px`;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,14 +105,8 @@ export function ColorTool(props: ColorToolProps) {
|
||||
}}
|
||||
cacheKey={`custom_${cacheKey}`}
|
||||
onChange={(color) => {
|
||||
const currentColor = config.get(cacheKey);
|
||||
if (currentColor && currentColor === color) {
|
||||
onColorChange();
|
||||
config.set(cacheKey, null);
|
||||
} else {
|
||||
onColorChange(color);
|
||||
config.set(cacheKey, color);
|
||||
}
|
||||
onColorChange(color);
|
||||
config.set(cacheKey, color);
|
||||
}}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title={title}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
.ProseMirror p span {
|
||||
.ProseMirror span * {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,15 +68,8 @@ const error: ThemeUIStyleObject = {
|
||||
}
|
||||
};
|
||||
|
||||
const radio: ThemeUIStyleObject = {
|
||||
"input:focus ~ &": {
|
||||
backgroundColor: `border-secondary`
|
||||
}
|
||||
};
|
||||
|
||||
export const inputVariants = {
|
||||
input: defaultVariant,
|
||||
error,
|
||||
clean,
|
||||
radio
|
||||
clean
|
||||
};
|
||||
|
||||
@@ -25,9 +25,9 @@ import parser from "yargs-parser";
|
||||
import { fdir } from "fdir";
|
||||
import Listr from "listr";
|
||||
|
||||
const THREADS = Math.max(4, process.env.THREADS || os.cpus().length / 2);
|
||||
const args = parser(process.argv, { alias: { scope: ["s"], offline: ["o"] } });
|
||||
const IS_CI = process.env.CI;
|
||||
const THREADS = Math.max(4, process.env.THREADS || os.cpus().length / 2);
|
||||
const scopes = {
|
||||
mobile: "apps/mobile",
|
||||
web: "apps/web",
|
||||
@@ -38,14 +38,6 @@ const scopes = {
|
||||
themes: "servers/themes",
|
||||
themebuilder: "apps/theme-builder"
|
||||
};
|
||||
// packages that we shouldn't run npm rebuild for
|
||||
const IGNORED_NATIVE_PACKAGES = [
|
||||
// optional dependency of pdfjs-dist, we can ignore
|
||||
// it because it's only needed in non-browser environments
|
||||
"canvas",
|
||||
// optional dependency only used on Node.js platform
|
||||
"@azure/msal-node-runtime"
|
||||
];
|
||||
|
||||
if (args.scope && !scopes[args.scope])
|
||||
throw new Error(`Scope must be one of ${Object.keys(scopes).join(", ")}`);
|
||||
@@ -103,10 +95,12 @@ async function bootstrapPackages(dependencies) {
|
||||
console.timeEnd("Took");
|
||||
}
|
||||
|
||||
function execute(cmd, cwd, outputs) {
|
||||
function bootstrapPackage(cwd, outputs) {
|
||||
return new Promise((resolve, reject) =>
|
||||
exec(
|
||||
cmd,
|
||||
`npm ${IS_CI ? "ci" : "i"} --legacy-peer-deps --no-audit --no-fund ${
|
||||
args.offline ? "--offline" : "--prefer-offline"
|
||||
} --progress=false`,
|
||||
{
|
||||
cwd,
|
||||
env: process.env,
|
||||
@@ -115,6 +109,7 @@ function execute(cmd, cwd, outputs) {
|
||||
(err, stdout, stderr) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
outputs.stdout.push("> " + cwd);
|
||||
outputs.stdout.push(stdout);
|
||||
outputs.stderr.push(stderr);
|
||||
|
||||
@@ -124,32 +119,6 @@ function execute(cmd, cwd, outputs) {
|
||||
);
|
||||
}
|
||||
|
||||
async function bootstrapPackage(cwd, outputs) {
|
||||
const cmd = `npm ${
|
||||
IS_CI ? "ci" : "i"
|
||||
} --legacy-peer-deps --no-audit --no-fund ${
|
||||
args.offline ? "--offline" : "--prefer-offline"
|
||||
} --progress=false --ignore-scripts`;
|
||||
|
||||
outputs.stdout.push("> " + cwd);
|
||||
|
||||
await execute(cmd, cwd, outputs);
|
||||
|
||||
const postInstallCommands = [];
|
||||
|
||||
const packages = await needsRebuild(cwd);
|
||||
if (packages.length > 0) {
|
||||
postInstallCommands.push(`npm rebuild ${packages.join(" ")}`);
|
||||
}
|
||||
|
||||
if (await hasScript(cwd, "postinstall"))
|
||||
postInstallCommands.push(`npm run postinstall `);
|
||||
|
||||
for (const cmd of postInstallCommands) {
|
||||
await execute(cmd, cwd, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
async function findDependencies(scope) {
|
||||
try {
|
||||
const packageJsonPath = path.join(scope, "package.json");
|
||||
@@ -182,39 +151,3 @@ function filterDependencies(basePath, dependencies) {
|
||||
path.resolve(path.join(basePath, value.replace("file:", "")))
|
||||
);
|
||||
}
|
||||
|
||||
async function needsRebuild(cwd) {
|
||||
const scripts = ["preinstall", "install", "postinstall"];
|
||||
const packages = await new fdir()
|
||||
.glob("**/package.json")
|
||||
.withFullPaths()
|
||||
.crawl(path.join(cwd, "node_modules"))
|
||||
.withPromise();
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
packages.map(async (path) => {
|
||||
const pkg = await readFile(path, "utf-8")
|
||||
.then(JSON.parse)
|
||||
.catch(Object);
|
||||
|
||||
if (
|
||||
!pkg ||
|
||||
!pkg.scripts ||
|
||||
IGNORED_NATIVE_PACKAGES.includes(pkg.name) ||
|
||||
!scripts.some((s) => pkg.scripts[s])
|
||||
)
|
||||
return;
|
||||
|
||||
return pkg.name;
|
||||
})
|
||||
)
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
async function hasScript(cwd, scriptName) {
|
||||
const pkg = await readFile(path.join(cwd, "package.json"), "utf-8")
|
||||
.then(JSON.parse)
|
||||
.catch(Object);
|
||||
return pkg && pkg.scripts && pkg.scripts[scriptName];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user