mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +01:00
Merge branch 'release/2.1.1'
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
66
apps/web/src/components/dialogs/migration-dialog.tsx
Normal file
66
apps/web/src/components/dialogs/migration-dialog.tsx
Normal 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 & 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 &
|
||||
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 & 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>
|
||||
);
|
||||
}
|
||||
@@ -255,7 +255,7 @@ function DiffViewer(props) {
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Flex sx={{ px: 2 }}>
|
||||
<Flex sx={{ px: 2, overflow: "auto" }}>
|
||||
<Editor
|
||||
content={htmlDiff.after}
|
||||
nonce={0}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,6 +69,7 @@ function TipTap(props: TipTapProps) {
|
||||
|
||||
const editor = useTiptap(
|
||||
{
|
||||
isKeyboardOpen: true,
|
||||
isMobile: isMobile || false,
|
||||
element: editorContainer,
|
||||
editable: !readonly,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
})();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
36
apps/web/src/utils/logger.ts
Normal file
36
apps/web/src/utils/logger.ts
Normal 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 };
|
||||
@@ -31,6 +31,7 @@ function showToast(
|
||||
position: "bottom-right",
|
||||
style: {
|
||||
maxWidth: "auto",
|
||||
backgroundColor: "var(--background)",
|
||||
},
|
||||
});
|
||||
return { hide: () => toast.dismiss(id) };
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user