Merge branch 'release/2.1.1'

This commit is contained in:
thecodrr
2022-07-20 09:46:30 +05:00
21 changed files with 209 additions and 43 deletions

View File

@@ -233,7 +233,7 @@ test.skip("last line doesn't get saved if it's font is different", async () => {
expect(content).toMatchSnapshot(`last-line-with-different-font.txt`);
});
test.only("editing a note and switching immediately to another note and making an edit shouldn't overlap both notes", async ({
test("editing a note and switching immediately to another note and making an edit shouldn't overlap both notes", async ({
page,
}, { setTimeout }) => {
await createNoteAndCheckPresence({

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.1.0",
"version": "2.1.1",
"private": true,
"main": "./build/electron.js",
"homepage": "https://notesnook.com/",
@@ -37,7 +37,7 @@
"build": {
"appId": "com.streetwriters.notesnook",
"productName": "Notesnook",
"copyright": "Copyright © 2021 Streetwriters (Private) Ltd.",
"copyright": "Copyright © 2022 Streetwriters (Private) Ltd.",
"artifactName": "notesnook_${arch}.${ext}",
"files": [
"!*.chunk.js.map",

View File

@@ -1,7 +1,7 @@
{
"name": "notesnook",
"description": "Your private note taking space",
"version": "2.1.0",
"version": "2.1.1",
"private": true,
"main": "./src/App.js",
"homepage": "https://notesnook.com/",
@@ -16,8 +16,8 @@
"@notesnook/crypto": "^1.0.0",
"@notesnook/desktop": "file:desktop",
"@rebass/forms": "^4.0.6",
"@streetwriters/editor": "^1.0.3",
"@streetwriters/notesnook-core": "^7.0.5",
"@streetwriters/editor": "^1.1.2",
"@streetwriters/notesnook-core": "^7.2.1",
"@streetwriters/theme": "1.0.0",
"allotment": "^1.12.1",
"async-mutex": "^0.3.2",

View File

@@ -1,18 +1,19 @@
import { EventSourcePolyfill as EventSource } from "event-source-polyfill";
import { EVENTS } from "@streetwriters/notesnook-core/common";
import { TaskManager } from "./task-manager";
import { NNStorage } from "../interfaces/storage";
import { logger } from "../utils/logger";
/**
* @type {import("@streetwriters/notesnook-core/api").default}
*/
var db;
async function initializeDatabase(persistence) {
logger.measure("Database initialization");
const { default: Database } = await import(
"@streetwriters/notesnook-core/api"
);
const { NNStorage } = await import("../interfaces/storage");
const { default: FS } = await import("../interfaces/fs");
db = new Database(new NNStorage(persistence), EventSource, FS);
db = new Database(new NNStorage("Notesnook", persistence), EventSource, FS);
// if (isTesting()) {
db.host({
@@ -36,22 +37,18 @@ async function initializeDatabase(persistence) {
// });
// }
db.eventManager.subscribe(EVENTS.databaseMigrating, async ({ from, to }) => {
await TaskManager.startTask({
type: "modal",
title: `Migrating your database`,
subtitle:
"Please do not close your browser/app before the migration is done.",
action: (task) => {
task({ text: `Migrating database from v${from} to v${to}` });
return new Promise((resolve) => {
db.eventManager.subscribe(EVENTS.databaseMigrated, resolve);
});
},
});
});
// db.eventManager.subscribe(EVENTS.databaseMigrating, async ({ from, to }) => {
// });
await db.init();
logger.measure("Database initialization");
if (db.migrations.required()) {
const { showMigrationDialog } = await import("./dialog-controller");
await showMigrationDialog();
}
return db;
}

View File

@@ -278,6 +278,12 @@ export function showEmailVerificationDialog() {
));
}
export function showMigrationDialog() {
return showDialog("MigrationDialog", (Dialog, perform) => (
<Dialog onClose={() => perform(false)} />
));
}
type LoadingDialogProps = {
title: string;
message?: string;

View File

@@ -14,6 +14,7 @@ import FileSaver from "file-saver";
import { showToast } from "../utils/toast";
import { SUBSCRIPTION_STATUS } from "./constants";
import { showFilePicker } from "../components/editor/picker";
import { logger } from "../utils/logger";
export const CREATE_BUTTON_MAP = {
notes: {
@@ -196,7 +197,7 @@ async function restore(backup, password) {
await db.backup.import(backup, password);
showToast("success", "Backup restored!");
} catch (e) {
console.error(e);
await showToast("error", `Could not restore the backup: ${e.message || e}`);
logger.error(e, "Could not restore the backup");
showToast("error", `Could not restore the backup: ${e.message || e}`);
}
}

View File

@@ -19,6 +19,7 @@ import OnboardingDialog from "./onboarding-dialog";
import AttachmentsDialog from "./attachmentsdialog";
import { Prompt } from "./prompt";
import { ToolbarConfigDialog } from "./toolbarconfigdialog";
import { MigrationDialog } from "./migrationdialog";
const Dialogs = {
AddNotebookDialog,
@@ -43,5 +44,6 @@ const Dialogs = {
RecoveryCodesDialog,
OnboardingDialog,
AttachmentsDialog,
MigrationDialog,
};
export default Dialogs;

View File

@@ -0,0 +1,66 @@
import { Text } from "rebass";
import { createBackup } from "../../common";
import { db } from "../../common/db";
import { Perform } from "../../common/dialog-controller";
import { TaskManager } from "../../common/task-manager";
import Dialog from "./dialog";
export type MigrationDialogProps = {
onClose: Perform;
};
export function MigrationDialog(props: MigrationDialogProps) {
return (
<Dialog
width={500}
isOpen={true}
title={"Database migration required"}
description={
"Due to new features we need to migrate your data to a newer version. This is NOT a destructive operation."
}
positiveButton={{
text: "Backup and migrate",
onClick: async () => {
await createBackup(true);
await TaskManager.startTask({
type: "modal",
title: `Migrating your database`,
subtitle:
"Please do NOT close your browser/app during the migration process.",
action: (task) => {
task({ text: `Please wait...` });
return db.migrations?.migrate();
},
});
props.onClose(true);
},
}}
>
<Text variant={"subtitle"}>Read before continuing:</Text>
<Text as="ol" sx={{ paddingInlineStart: 20, mt: 1 }}>
<Text as="li" variant={"body"}>
It is <b>required</b> that you <b>download &amp; save a backup</b> of
your data.
</Text>
<Text as="li" variant={"body"}>
Some <b>merge conflicts</b> in your notes after a migration are
expected. It is <b>recommended</b> that you go through them &amp;
resolve them carefully.
<Text as="ol" sx={{ paddingInlineStart: 20 }}>
<Text as="li" variant={"body"}>
<b>But if you are feeling reckless</b> and want to risk losing
some data, you can logout &amp; log back in.
</Text>
</Text>
</Text>
<Text as="li" variant={"body"}>
If you face any other issues or are unsure about what to do, feel free
to reach out to us via{" "}
<a href="https://discord.com/invite/zQBK97EE22">Discord</a> or email
us at{" "}
<a href="mailto:support@streetwriters.co">support@streetwriters.co</a>
</Text>
</Text>
</Dialog>
);
}

View File

@@ -255,7 +255,7 @@ function DiffViewer(props) {
}}
/>
<ScrollSyncPane>
<Flex sx={{ px: 2 }}>
<Flex sx={{ px: 2, overflow: "auto" }}>
<Editor
content={htmlDiff.after}
nonce={0}

View File

@@ -68,6 +68,7 @@ export default function EditorManager({
const title = useRef<string>("");
const previewSession = useRef<PreviewSession>();
const [dropRef, overlayRef] = useDragOverlay();
const editor = useEditorInstance();
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible);
const toggleProperties = useStore((store) => store.toggleProperties);
@@ -128,6 +129,21 @@ export default function EditorManager({
alignSelf: "stretch",
overflow: "hidden",
}}
onPaste={async (event) => {
if (!editor) return;
if (event.clipboardData?.items?.length) {
event.preventDefault();
for (let item of event.clipboardData.items) {
const file = item.getAsFile();
if (!file) continue;
const result = await attachFile(file);
if (!result) continue;
editor.attachFile(result);
}
}
}}
>
{previewSession.current && (
<PreviewModeNotice

View File

@@ -69,6 +69,7 @@ function TipTap(props: TipTapProps) {
const editor = useTiptap(
{
isKeyboardOpen: true,
isMobile: isMobile || false,
element: editorContainer,
editable: !readonly,

View File

@@ -5,6 +5,7 @@ import TimeAgo from "../time-ago";
import ListItem from "../list-item";
import { confirm, showMoveNoteDialog } from "../../common/dialog-controller";
import { store, useStore } from "../../stores/note-store";
import { store as userstore } from "../../stores/user-store";
import { useStore as useAttachmentStore } from "../../stores/attachment-store";
import { db } from "../../common/db";
import { showUnpinnedToast } from "../../common/toasts";
@@ -366,6 +367,7 @@ const menuItems = [
},
{
key: "sync-disable",
hidden: () => !userstore.get().isLoggedIn,
disabled: ({ note }) =>
!db.notes.note(note.id).synced() ? notFullySyncedText : false,
title: ({ note }) => (note.localOnly ? "Enable sync" : "Disable sync"),

View File

@@ -18,7 +18,8 @@ export default function useDatabase(persistence) {
(async () => {
await import("../app.css");
await initializeDatabase(persistence);
if (process.env.NODE_ENV !== "development")
await initializeDatabase(persistence);
setIsAppLoaded(true);
memory.isAppLoaded = true;
})();

View File

@@ -10,6 +10,9 @@ import Config from "./utils/config";
import { isTesting } from "./utils/platform";
import { getServiceWorkerVersion } from "./utils/version";
import { initializeDatabase } from "./common/db";
import { initalizeLogger, logger } from "./utils/logger";
initalizeLogger();
if (process.env.REACT_APP_PLATFORM === "desktop") require("./commands");
const ROUTES = {
@@ -56,8 +59,12 @@ const sessionExpiryExceptions = [
function getRoute() {
const path = getCurrentPath();
logger.info(`Getting route for path: ${path}`);
const isSessionExpired = Config.get("sessionExpired", false);
if (isSessionExpired && !sessionExpiryExceptions.includes(path)) {
logger.info(`User session has expired. Routing to /sessionexpired`);
window.history.replaceState(
{},
null,
@@ -81,11 +88,13 @@ if (process.env.NODE_ENV === "development") {
function renderApp() {
const route = getRoute();
logger.measure("app render");
return route?.component()?.then(({ default: Component }) => {
render(
<Component {...route.props} />,
document.getElementById("root"),
() => {
logger.measure("app render");
document.getElementById("splash").remove();
}
);

View File

@@ -2,23 +2,25 @@ import localforage from "localforage";
import { extendPrototype } from "localforage-getitems";
import * as MemoryDriver from "localforage-driver-memory";
import { getNNCrypto } from "./nncrypto.stub";
import { Cipher, SerializedKey } from "@notesnook/crypto/dist/src/types";
type EncryptedKey = { iv: Uint8Array; cipher: BufferSource };
import type { Cipher, SerializedKey } from "@notesnook/crypto/dist/src/types";
localforage.defineDriver(MemoryDriver);
extendPrototype(localforage);
type EncryptedKey = { iv: Uint8Array; cipher: BufferSource };
export type DatabasePersistence = "memory" | "db";
const APP_SALT = "oVzKtazBo7d8sb7TBvY9jw";
export class NNStorage {
database: LocalForage;
constructor(persistence: "memory" | "db" = "db") {
constructor(name: string, persistence: DatabasePersistence = "db") {
const drivers =
persistence === "memory"
? [MemoryDriver._driver]
: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE];
this.database = localforage.createInstance({
name: "Notesnook",
name,
driver: drivers,
});
}

View File

@@ -9,7 +9,8 @@ import { store as attachmentStore } from "./attachment-store";
import BaseStore from "./index";
import { showToast } from "../utils/toast";
import { resetReminders } from "../common/reminders";
import { EVENTS } from "@streetwriters/notesnook-core/common";
import { EV, EVENTS } from "@streetwriters/notesnook-core/common";
import { logger } from "../utils/logger";
var syncStatusTimeout = 0;
const BATCH_SIZE = 50;
@@ -32,6 +33,8 @@ class AppStore extends BaseStore {
init = () => {
let count = 0;
EV.subscribe(EVENTS.appRefreshRequested, () => this.refresh());
db.eventManager.subscribe(
EVENTS.syncProgress,
({ type, total, current }) => {
@@ -61,6 +64,8 @@ class AppStore extends BaseStore {
};
refresh = async () => {
logger.measure("refreshing app");
await this.updateLastSynced();
await resetReminders();
noteStore.refresh();
@@ -69,6 +74,8 @@ class AppStore extends BaseStore {
tagStore.refresh();
attachmentStore.refresh();
this.refreshNavItems();
logger.measure("refreshing app");
};
refreshNavItems = () => {
@@ -180,10 +187,9 @@ class AppStore extends BaseStore {
else if (full) this.updateSyncStatus("completed");
await this.updateLastSynced();
return await this.refresh();
})
.catch(async (err) => {
console.error(err);
logger.error(err);
if (err.code === "MERGE_CONFLICT") {
if (editorstore.get().session.id)
editorstore.openSession(editorstore.get().session.id, true);
@@ -211,6 +217,7 @@ class AppStore extends BaseStore {
* @param {"synced" | "syncing" | "conflicts" | "failed" | "completed"} key
*/
updateSyncStatus = (key) => {
logger.info(`Sync status updated: ${key}`);
this.set((state) => (state.syncStatus.key = key));
};
}

View File

@@ -7,6 +7,7 @@ import { db } from "../common/db";
import BaseStore from ".";
import { EV, EVENTS } from "@streetwriters/notesnook-core/common";
import { hashNavigate } from "../navigation";
import { logger } from "../utils/logger";
const SESSION_STATES = {
stale: "stale",
@@ -121,12 +122,14 @@ class EditorStore extends BaseStore {
saveSession = async (sessionId, session) => {
const currentSession = this.get().session;
if (currentSession.readonly && session.readonly !== false) return; // do not allow saving of readonly session
if (currentSession.saveState === 0) return;
this.setSaveState(0);
try {
const id = await this._getSaveFn()({ ...session, id: sessionId });
if (currentSession && currentSession.id !== sessionId) return;
if (currentSession && currentSession.id !== sessionId)
throw new Error("Aborting save operation: old session.");
let note = db.notes.note(id)?.data;
if (!note) throw new Error("Note not saved.");
@@ -173,9 +176,10 @@ class EditorStore extends BaseStore {
noteStore.setSelectedNote(id);
hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
}
this.setSaveState(1);
} catch (err) {
this.setSaveState(-1);
console.error(err);
logger.info(err);
if (session.locked) {
hashNavigate(`/notes/${session.id}/unlock`, { replace: true });
}
@@ -300,3 +304,7 @@ class EditorStore extends BaseStore {
*/
const [useStore, store] = createStore(EditorStore);
export { useStore, store, SESSION_STATES };
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -14,6 +14,7 @@ import { hashNavigate } from "../navigation";
import { isUserPremium } from "../hooks/use-is-user-premium";
import { SUBSCRIPTION_STATUS } from "../common/constants";
import { ANALYTICS_EVENTS, trackEvent } from "../utils/analytics";
import { logger } from "../utils/logger";
class UserStore extends BaseStore {
isLoggedIn = false;
@@ -25,8 +26,6 @@ class UserStore extends BaseStore {
user = undefined;
init = () => {
EV.subscribe(EVENTS.appRefreshRequested, () => appStore.refresh());
EV.subscribe(EVENTS.userSessionExpired, async () => {
Config.set("sessionExpired", true);
window.location.replace("/sessionexpired");
@@ -81,14 +80,13 @@ class UserStore extends BaseStore {
onPageVisibilityChanged(async function (type, documentHidden) {
if (!documentHidden) {
logger.info("Page visibility changed. Reconnecting SSE...");
if (type === "online") {
// a slight delay to make sure sockets are open and can be connected
// to. Otherwise, this fails miserably.
await new Promise((resolve) => setTimeout(resolve, 2000));
}
await db
.connectSSE({ force: type === "online" })
.catch(console.error);
await db.connectSSE({ force: type === "online" }).catch(logger.error);
}
});

View File

@@ -0,0 +1,36 @@
import {
initalize,
logger as _logger,
logManager,
} from "@streetwriters/notesnook-core/logger";
import FileSaver from "file-saver";
import { LogMessage } from "react-virtuoso/dist/loggerSystem";
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
import { zip } from "./zip";
var logger: typeof _logger;
function initalizeLogger(persistence: DatabasePersistence = "db") {
initalize(new NNStorage("Logs", persistence) as any);
logger = _logger.scope("notesnook-web");
}
async function downloadLogs() {
if (!logManager) return;
const allLogs = await logManager.get();
const files = allLogs.map((log) => ({
filename: log.key,
content: (log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n"),
}));
const archive = await zip(files, "log");
FileSaver.saveAs(new Blob([archive.buffer]), "notesnook-logs.zip");
}
async function clearLogs() {
if (!logManager) return;
await logManager.clear();
}
export { initalizeLogger, logger, downloadLogs, clearLogs };

View File

@@ -31,6 +31,7 @@ function showToast(
position: "bottom-right",
style: {
maxWidth: "auto",
backgroundColor: "var(--background)",
},
});
return { hide: () => toast.dismiss(id) };

View File

@@ -44,6 +44,7 @@ import { PATHS } from "@notesnook/desktop/paths";
import { openPath } from "../commands/open";
import { getAllAccents } from "@streetwriters/theme";
import { debounce } from "../utils/debounce";
import { clearLogs, downloadLogs } from "../utils/logger";
function subscriptionStatusToString(user) {
const status = user?.subscription?.type;
@@ -656,6 +657,18 @@ function Settings(props) {
onToggled={() => setDebugMode(!debugMode)}
isToggled={debugMode}
/>
<Button variant="list" onClick={downloadLogs}>
<Tip
text="Download logs"
tip="Logs are stored locally & do not contain any sensitive information."
/>
</Button>
<Button variant="list" onClick={clearLogs}>
<Tip
text="Clear logs"
tip="Clear all logs stored in the database."
/>
</Button>
{isDesktop() && (
<Button
variant="list"