diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 61e8b42c2..5d095392c 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -177,7 +177,6 @@ }, "node_modules/@electron/get": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -804,7 +803,6 @@ }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -815,7 +813,6 @@ }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -857,7 +854,6 @@ }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -894,12 +890,10 @@ }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", - "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -918,7 +912,6 @@ }, "node_modules/@types/node": { "version": "18.15.11", - "devOptional": true, "license": "MIT" }, "node_modules/@types/normalize-package-data": { @@ -938,7 +931,6 @@ }, "node_modules/@types/responselike": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -969,7 +961,6 @@ }, "node_modules/@types/yauzl": { "version": "2.10.0", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1323,7 +1314,6 @@ }, "node_modules/boolean": { "version": "3.2.0", - "dev": true, "license": "MIT", "optional": true }, @@ -1386,7 +1376,6 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -1564,7 +1553,6 @@ }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" @@ -1572,7 +1560,6 @@ }, "node_modules/cacheable-request": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -1708,7 +1695,6 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -1863,7 +1849,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -1877,7 +1862,6 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1899,7 +1883,6 @@ }, "node_modules/defer-to-connect": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1907,7 +1890,6 @@ }, "node_modules/define-properties": { "version": "1.2.0", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1952,7 +1934,6 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "dev": true, "license": "MIT", "optional": true }, @@ -2100,7 +2081,6 @@ "version": "24.5.0", "resolved": "https://registry.npmjs.org/electron/-/electron-24.5.0.tgz", "integrity": "sha512-9Xo2EFZHWeuw1otm9mcJYKCNC64fPRpgp+ZJWMJ9RtvsnSgcuitkM4esZv4gIsqhWk5yiKApYHqinIUyu82O0Q==", - "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", @@ -2454,7 +2434,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -2462,7 +2441,6 @@ }, "node_modules/env-paths": { "version": "2.2.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2483,7 +2461,6 @@ }, "node_modules/es6-error": { "version": "4.1.1", - "dev": true, "license": "MIT", "optional": true }, @@ -2533,7 +2510,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2557,7 +2533,6 @@ }, "node_modules/extract-zip": { "version": "2.0.1", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -2595,7 +2570,6 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" @@ -2671,7 +2645,6 @@ }, "node_modules/fs-extra": { "version": "8.1.0", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -2700,7 +2673,7 @@ }, "node_modules/function-bind": { "version": "1.1.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/gauge": { @@ -2730,7 +2703,6 @@ }, "node_modules/get-intrinsic": { "version": "1.2.0", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2744,7 +2716,6 @@ }, "node_modules/get-stream": { "version": "5.2.0", - "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -2788,7 +2759,6 @@ }, "node_modules/global-agent": { "version": "3.0.0", - "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -2805,7 +2775,6 @@ }, "node_modules/global-agent/node_modules/semver": { "version": "7.3.8", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -2820,7 +2789,6 @@ }, "node_modules/globalthis": { "version": "1.0.3", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2835,7 +2803,6 @@ }, "node_modules/got": { "version": "11.8.6", - "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -2868,7 +2835,7 @@ }, "node_modules/has": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1" @@ -2887,7 +2854,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.0", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2899,7 +2865,6 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2927,7 +2892,6 @@ }, "node_modules/http-cache-semantics": { "version": "4.1.0", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/http-proxy-agent": { @@ -2945,7 +2909,6 @@ }, "node_modules/http2-wrapper": { "version": "1.0.3", - "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -3224,7 +3187,6 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "dev": true, "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -3239,7 +3201,6 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "dev": true, "license": "ISC", "optional": true }, @@ -3256,7 +3217,6 @@ }, "node_modules/jsonfile": { "version": "4.0.0", - "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -3264,7 +3224,6 @@ }, "node_modules/keyv": { "version": "4.5.2", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -3323,7 +3282,6 @@ }, "node_modules/lowercase-keys": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3375,7 +3333,6 @@ }, "node_modules/matcher": { "version": "3.0.0", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3425,7 +3382,6 @@ }, "node_modules/mimic-response": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3695,7 +3651,6 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3720,7 +3675,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -3729,7 +3683,6 @@ }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3773,7 +3726,6 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3888,7 +3840,6 @@ }, "node_modules/pend": { "version": "1.2.0", - "dev": true, "license": "MIT" }, "node_modules/picomatch": { @@ -3923,7 +3874,6 @@ }, "node_modules/progress": { "version": "2.0.3", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3948,7 +3898,6 @@ }, "node_modules/pump": { "version": "3.0.0", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -3965,7 +3914,6 @@ }, "node_modules/quick-lru": { "version": "5.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4141,12 +4089,10 @@ }, "node_modules/resolve-alpn": { "version": "1.2.1", - "dev": true, "license": "MIT" }, "node_modules/responselike": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -4191,7 +4137,6 @@ }, "node_modules/roarr": { "version": "2.15.4", - "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -4251,7 +4196,6 @@ }, "node_modules/semver": { "version": "6.3.0", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4259,13 +4203,11 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "dev": true, "license": "MIT", "optional": true }, "node_modules/serialize-error": { "version": "7.0.1", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4280,7 +4222,6 @@ }, "node_modules/serialize-error/node_modules/type-fest": { "version": "0.13.1", - "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { @@ -4434,7 +4375,6 @@ }, "node_modules/sprintf-js": { "version": "1.1.2", - "dev": true, "license": "BSD-3-Clause", "optional": true }, @@ -4512,7 +4452,6 @@ }, "node_modules/sumchecker": { "version": "3.0.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" @@ -4710,7 +4649,6 @@ }, "node_modules/universalify": { "version": "0.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -4803,7 +4741,6 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/xmlbuilder": { @@ -4850,7 +4787,6 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", diff --git a/apps/desktop/scripts/dev.ts b/apps/desktop/scripts/dev.ts index d80d0a55a..f80666df5 100644 --- a/apps/desktop/scripts/dev.ts +++ b/apps/desktop/scripts/dev.ts @@ -56,7 +56,7 @@ async function main() { await fs.rm("./build/", { force: true, recursive: true }); } - await $`npm run build:electron`; + await $`npm run bundle`; await $`tsc`; if (first) { @@ -70,7 +70,7 @@ async function main() { await spawnAndWaitUntil( path.join(__dirname, "..", "..", "web"), "npm run start:desktop", - (data) => data.includes("Compiled successfully!") + (data) => data.includes("Network: use --host to expose") ); isServerRunning = true; } diff --git a/apps/desktop/src/api/spell-checker.ts b/apps/desktop/src/api/spell-checker.ts index f4c7587b9..16d64306f 100644 --- a/apps/desktop/src/api/spell-checker.ts +++ b/apps/desktop/src/api/spell-checker.ts @@ -113,8 +113,10 @@ export const spellCheckerRouter = t.router({ .mutation(({ input: languages }) => globalThis.window?.webContents.session.setSpellCheckerLanguages(languages) ), - toggle: t.procedure.input(z.boolean()).mutation(({ input: enabled }) => { - globalThis.window?.webContents.session.setSpellCheckerEnabled(enabled); - config.isSpellCheckerEnabled = enabled; - }) + toggle: t.procedure + .input(z.boolean().optional()) + .mutation(({ input: enabled }) => { + globalThis.window?.webContents.session.setSpellCheckerEnabled(!!enabled); + config.isSpellCheckerEnabled = !!enabled; + }) }); diff --git a/apps/web/__e2e__/backups.test.ts b/apps/web/__e2e__/backups.test.ts index 785f429a6..81f2b5f86 100644 --- a/apps/web/__e2e__/backups.test.ts +++ b/apps/web/__e2e__/backups.test.ts @@ -39,6 +39,7 @@ test("restore a backup", async ({ page }) => { await settings.restoreData("backup.nnbackup"); + await settings.close(); const notes = await app.goToNotes(); expect(await notes.isEmpty()).toBeFalsy(); const notebooks = await app.goToNotebooks(); @@ -65,6 +66,7 @@ test("restore an encrypted backup", async ({ page }) => { await settings.restoreData("encrypted.nnbackup", USER.CURRENT.password); + await settings.close(); const notes = await app.goToNotes(); expect(await notes.isEmpty()).toBeFalsy(); const notebooks = await app.goToNotebooks(); diff --git a/apps/web/__e2e__/models/app.model.ts b/apps/web/__e2e__/models/app.model.ts index 92c86f37e..ee4256e33 100644 --- a/apps/web/__e2e__/models/app.model.ts +++ b/apps/web/__e2e__/models/app.model.ts @@ -42,7 +42,7 @@ export class AppModel { constructor(page: Page) { this.page = page; this.toasts = new ToastsModel(page); - this.navigation = new NavigationMenuModel(page); + this.navigation = new NavigationMenuModel(page, "navigation-menu"); this.auth = new AuthModel(page); this.checkout = new CheckoutModel(page); this.routeHeader = this.page.locator(getTestId("routeHeader")); diff --git a/apps/web/__e2e__/models/navigation-menu.model.ts b/apps/web/__e2e__/models/navigation-menu.model.ts index 0f25d45e8..062de71d7 100644 --- a/apps/web/__e2e__/models/navigation-menu.model.ts +++ b/apps/web/__e2e__/models/navigation-menu.model.ts @@ -26,9 +26,9 @@ export class NavigationMenuModel { protected readonly page: Page; private readonly menu: Locator; - constructor(page: Page) { + constructor(page: Page, id: string) { this.page = page; - this.menu = page.locator(getTestId("navigation-menu")); + this.menu = page.locator(getTestId(id)); } async findItem(title: string) { diff --git a/apps/web/__e2e__/models/settings-view.model.ts b/apps/web/__e2e__/models/settings-view.model.ts index 51d9bbfb3..15a26a341 100644 --- a/apps/web/__e2e__/models/settings-view.model.ts +++ b/apps/web/__e2e__/models/settings-view.model.ts @@ -17,42 +17,52 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Locator, Page } from "@playwright/test"; +import { Page } from "@playwright/test"; import { downloadAndReadFile, getTestId, uploadFile } from "../utils"; import { confirmDialog, fillPasswordDialog, waitToHaveText } from "./utils"; +import { NavigationMenuModel } from "./navigation-menu.model"; export class SettingsViewModel { private readonly page: Page; - private readonly logoutButton: Locator; - private readonly accountStatusContainer: Locator; - private readonly backupRecoveryKeyButton: Locator; - private readonly backupRestoreContainer: Locator; - private readonly backupData: Locator; - private readonly restoreBackup: Locator; - private readonly encyptBackups: Locator; + private readonly navigation: NavigationMenuModel; + constructor(page: Page) { this.page = page; - this.logoutButton = page.locator(getTestId("settings-logout")); - this.accountStatusContainer = page.locator(getTestId("account-status")); - this.backupRecoveryKeyButton = page.locator( - getTestId("backup-recovery-key") - ); - this.backupRestoreContainer = page.locator(getTestId("backup-restore")); - this.backupData = page.locator(getTestId("backup-data")); - this.restoreBackup = page.locator(getTestId("restore-backup")); - this.encyptBackups = page.locator(getTestId("encrypt-backups")); + this.navigation = new NavigationMenuModel(page, "settings-navigation-menu"); + } + + async close() { + await this.page.locator(getTestId("settings-search")).focus(); + await this.page.waitForTimeout(100); + await this.page.keyboard.press("Escape"); + await this.page.waitForTimeout(1000); } async logout() { - await this.logoutButton.click(); + const item = await this.navigation.findItem("Profile"); + await item?.click(); + + const logoutButton = this.page + .locator(getTestId("setting-logout")) + .locator("button"); + + await logoutButton.click(); await confirmDialog(this.page); + await this.page .locator(getTestId("not-logged-in")) .waitFor({ state: "visible" }); } async getRecoveryKey(password: string) { - await this.backupRecoveryKeyButton.click(); + const item = await this.navigation.findItem("Profile"); + await item?.click(); + + const backupRecoveryKeyButton = this.page + .locator(getTestId("setting-recovery-key")) + .locator("button"); + + await backupRecoveryKeyButton.click(); await fillPasswordDialog(this.page, password); await waitToHaveText(this.page, "recovery-key"); @@ -65,21 +75,42 @@ export class SettingsViewModel { } async isLoggedIn() { - return await this.accountStatusContainer.isVisible(); + const item = await this.navigation.findItem("Subscription"); + return !!(await item?.getTitle()); } async createBackup(password?: string) { - await this.backupRestoreContainer.click(); - if (password) await this.encyptBackups.click(); - await this.backupData.click(); - if (password) await fillPasswordDialog(this.page, password); - return await downloadAndReadFile(this.page, this.backupData, "utf-8"); + const item = await this.navigation.findItem("Backup & export"); + await item?.click(); + + if (password) { + const encyptBackups = this.page + .locator(getTestId("setting-encrypt-backups")) + .locator("label"); + await encyptBackups.click(); + } + + const backupData = this.page + .locator(getTestId("setting-create-backup")) + .locator("button"); + + if (password) { + await backupData.click(); + await fillPasswordDialog(this.page, password); + } + + return await downloadAndReadFile(this.page, backupData, "utf-8"); } async restoreData(filename: string, password?: string) { - await this.backupRestoreContainer.click(); - await this.restoreBackup.click(); - await uploadFile(this.page, this.restoreBackup, filename); + const item = await this.navigation.findItem("Backup & export"); + await item?.click(); + + const restoreBackup = this.page + .locator(getTestId("setting-restore-backup")) + .locator("button"); + + await uploadFile(this.page, restoreBackup, filename); if (password) await fillPasswordDialog(this.page, password); } } diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index a2f146744..a26c149db 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -59,11 +59,13 @@ "w3c-keyname": "^2.2.6", "web-streams-polyfill": "^3.1.1", "wouter": "2.7.3", - "zustand": "^3.3.1" + "zustand": "^4.3.8" }, "devDependencies": { + "@babel/core": "^7.22.5", "@playwright/test": "^1.35.0", "@trpc/server": "^10.29.1", + "@types/babel__core": "^7.20.1", "@types/file-saver": "^2.0.5", "@types/marked": "^4.0.7", "@types/node-fetch": "^2.5.10", @@ -71,6 +73,8 @@ "@types/react": "17.0.2", "@types/react-dom": "17.0.2", "@types/react-modal": "^3.13.1", + "@vitejs/plugin-react": "^4.0.0", + "@vitejs/plugin-react-refresh": "^1.3.6", "@vitejs/plugin-react-swc": "^3.3.2", "buffer": "^6.0.3", "chalk": "^4.1.0", @@ -1803,6 +1807,36 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.22.5.tgz", + "integrity": "sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz", + "integrity": "sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.20.5", "dev": true, @@ -2190,68 +2224,6 @@ "version": "2.5.0", "license": "0BSD" }, - "node_modules/@electron/get": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", - "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", - "peer": true, - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@electron/get/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.10.6", "license": "MIT", @@ -3341,18 +3313,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@standardnotes/auth": { "version": "3.19.4", "license": "AGPL-3.0-or-later", @@ -3743,18 +3703,6 @@ "node": ">=10" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "peer": true, - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@theme-ui/components": { "version": "0.14.7", "license": "MIT", @@ -3819,20 +3767,50 @@ "version": "10.29.1", "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.29.1.tgz", "integrity": "sha512-kNXgMh5ya+awuz2tB4eIyVrRs7nVtqGXwSGabzH3l5ZLWz7rbKJquOJ7h6bjvIfWUpaFG62HJNWxxGUtXCRgRw==", + "dev": true, "funding": [ "https://trpc.io/sponsor" ] }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "peer": true, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" } }, "node_modules/@types/chai": { @@ -3850,28 +3828,6 @@ "@types/chai": "*" } }, - "node_modules/@types/eslint": { - "version": "8.40.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.1.tgz", - "integrity": "sha512-vRb792M4mF1FBT+eoLecmkpLXwxsBHvWWRGJjzbYANBM6DtiJc6yETyv4rqDA6QNjF1pkj1U7LMA6dGb3VYlHw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.0", "dev": true, @@ -3882,26 +3838,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "peer": true - }, "node_modules/@types/json-schema": { "version": "7.0.11", "dev": true, "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/marked": { "version": "4.0.8", "dev": true, @@ -3909,6 +3850,7 @@ }, "node_modules/@types/node": { "version": "18.14.0", + "dev": true, "license": "MIT" }, "node_modules/@types/node-fetch": { @@ -3967,15 +3909,6 @@ "@types/node": "*" } }, - "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/styled-system": { "version": "5.1.16", "license": "MIT", @@ -3988,16 +3921,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", - "optional": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@virtuoso.dev/react-urx": { "version": "0.2.13", "license": "MIT", @@ -4015,6 +3938,60 @@ "version": "0.2.13", "license": "MIT" }, + "node_modules/@vitejs/plugin-react": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.0.tgz", + "integrity": "sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.4", + "@babel/plugin-transform-react-jsx-self": "^7.21.0", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0" + } + }, + "node_modules/@vitejs/plugin-react-refresh": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.6.tgz", + "integrity": "sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA==", + "deprecated": "This package has been deprecated in favor of @vitejs/plugin-react", + "dev": true, + "dependencies": { + "@babel/core": "^7.14.8", + "@babel/plugin-transform-react-jsx-self": "^7.14.5", + "@babel/plugin-transform-react-jsx-source": "^7.14.5", + "@rollup/pluginutils": "^4.1.1", + "react-refresh": "^0.10.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@vitejs/plugin-react-refresh/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vitejs/plugin-react-refresh/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.3.2.tgz", @@ -4027,6 +4004,15 @@ "vite": "^4" } }, + "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@vitest/expect": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.0.tgz", @@ -4108,181 +4094,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "peer": true - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4300,16 +4111,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -4594,13 +4395,6 @@ "version": "1.0.0", "license": "ISC" }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "optional": true, - "peer": true - }, "node_modules/brace-expansion": { "version": "1.1.11", "devOptional": true, @@ -4671,15 +4465,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "license": "BSD-3-Clause" @@ -4709,33 +4494,6 @@ "node": ">=8" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "peer": true, - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "peer": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.2", "dev": true, @@ -4878,16 +4636,6 @@ "license": "ISC", "optional": true }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/classnames": { "version": "2.3.2", "license": "MIT" @@ -4896,27 +4644,6 @@ "version": "3.0.3", "license": "MIT" }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "peer": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clone-response/node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -5128,6 +4855,7 @@ "node_modules/decompress-response": { "version": "6.0.0", "license": "MIT", + "optional": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -5165,18 +4893,9 @@ "node": ">=0.10.0" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/define-properties": { "version": "1.2.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "has-property-descriptors": "^1.0.0", @@ -5210,13 +4929,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "optional": true, - "peer": true - }, "node_modules/dom-serializer": { "version": "2.0.0", "license": "MIT", @@ -5293,24 +5005,6 @@ "node": ">=0.10.0" } }, - "node_modules/electron": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.1.0.tgz", - "integrity": "sha512-VKk4G/0euO7ysMKQKHXmI4d3/qR4uHsAtVFXK2WfQUVxBmc160OAm2R6PN9/EXmgXEioKQBtbc2/lvWyYpDbuA==", - "hasInstallScript": true, - "peer": true, - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.302", "dev": true, @@ -5346,24 +5040,11 @@ "node_modules/end-of-stream": { "version": "1.4.4", "license": "MIT", + "optional": true, "dependencies": { "once": "^1.4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", - "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "4.4.0", "license": "BSD-2-Clause", @@ -5389,15 +5070,6 @@ "node": ">=8.0.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/error-ex": { "version": "1.3.2", "license": "MIT", @@ -5451,13 +5123,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-module-lexer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", - "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", - "dev": true, - "peer": true - }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "dev": true, @@ -5487,13 +5152,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "optional": true, - "peer": true - }, "node_modules/esbuild": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", @@ -5549,53 +5207,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { "version": "1.0.1", "dev": true, @@ -5617,16 +5228,6 @@ "version": "5.0.0", "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/exenv": { "version": "1.2.2", "license": "BSD-3-Clause" @@ -5639,26 +5240,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "peer": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -5709,15 +5290,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "peer": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fflate": { "version": "0.7.4", "license": "MIT" @@ -6031,7 +5603,7 @@ }, "node_modules/get-intrinsic": { "version": "1.2.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.1", @@ -6047,21 +5619,6 @@ "dev": true, "license": "ISC" }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "peer": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.0", "dev": true, @@ -6101,31 +5658,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "peer": true - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "optional": true, - "peer": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/globals": { "version": "11.12.0", "dev": true, @@ -6136,7 +5668,7 @@ }, "node_modules/globalthis": { "version": "1.0.3", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.1.3" @@ -6166,33 +5698,9 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "peer": true, - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.10", + "dev": true, "license": "ISC" }, "node_modules/happy-dom": { @@ -6268,7 +5776,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.1" @@ -6290,7 +5798,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6363,25 +5871,6 @@ "entities": "^4.3.0" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "peer": true - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "peer": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -6777,37 +6266,6 @@ "node": ">=10" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -6832,12 +6290,6 @@ "node": ">=4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "peer": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "license": "MIT" @@ -6852,13 +6304,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "optional": true, - "peer": true - }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -6941,15 +6386,6 @@ "license": "MIT", "optional": true }, - "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "peer": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/leven": { "version": "3.1.0", "dev": true, @@ -6969,16 +6405,6 @@ "version": "1.2.4", "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, "node_modules/loader-utils": { "version": "2.0.4", "dev": true, @@ -7120,15 +6546,6 @@ "get-func-name": "^2.0.0" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -7183,19 +6600,6 @@ "node": ">= 12" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "optional": true, - "peer": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/md5-hex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", @@ -7253,6 +6657,7 @@ "node_modules/mimic-response": { "version": "3.1.0", "license": "MIT", + "optional": true, "engines": { "node": ">=10" }, @@ -7381,13 +6786,6 @@ "license": "MIT", "optional": true }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "peer": true - }, "node_modules/node-abi": { "version": "3.33.0", "license": "MIT", @@ -7442,18 +6840,6 @@ "node": ">=6" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -7493,7 +6879,7 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7518,6 +6904,7 @@ }, "node_modules/once": { "version": "1.4.0", + "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -7533,15 +6920,6 @@ "@otplib/preset-v11": "^12.0.1" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -7646,12 +7024,6 @@ "path2d-polyfill": "^2.0.1" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "peer": true - }, "node_modules/phone": { "version": "3.1.34", "license": "MIT", @@ -7805,15 +7177,6 @@ "version": "1.6.0", "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "peer": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -7831,6 +7194,7 @@ "node_modules/pump": { "version": "3.0.0", "license": "MIT", + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -7871,18 +7235,6 @@ ], "license": "MIT" }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -7907,6 +7259,7 @@ }, "node_modules/react": { "version": "17.0.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -7918,6 +7271,7 @@ }, "node_modules/react-dom": { "version": "17.0.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -8001,6 +7355,15 @@ "react-dom": ">=16.4.1" } }, + "node_modules/react-refresh": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", + "integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-scroll-sync": { "version": "0.9.0", "license": "MIT", @@ -8157,12 +7520,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "peer": true - }, "node_modules/resolve-from": { "version": "4.0.0", "license": "MIT", @@ -8170,18 +7527,6 @@ "node": ">=4" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "peer": true, - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -8206,24 +7551,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "optional": true, - "peer": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/rollup": { "version": "2.79.1", "dev": true, @@ -8333,6 +7660,7 @@ }, "node_modules/scheduler": { "version": "0.20.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -8398,13 +7726,6 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "optional": true, - "peer": true - }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", "license": "ISC", @@ -8419,32 +7740,6 @@ "version": "4.0.0", "license": "ISC" }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "optional": true, - "peer": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "dev": true, - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8579,13 +7874,6 @@ "version": "3.0.2", "license": "(WTFPL OR MIT)" }, - "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "optional": true, - "peer": true - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -8752,18 +8040,6 @@ "version": "4.1.3", "license": "MIT" }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "peer": true, - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -8796,16 +8072,6 @@ "integrity": "sha512-W9VYDkSgPMMKdhzMK2s0HVr36kvG/iI4sGCvkePPhop/hEbECFh5TaKVAIpDd9liWkmmGkKudeTDxZwZ31AwHg==", "dev": true }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/tar": { "version": "6.1.15", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", @@ -8918,41 +8184,6 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "dev": true, @@ -9048,19 +8279,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typed-array-length": { "version": "1.0.4", "dev": true, @@ -9202,6 +8420,14 @@ "react-dom": "16.8.0 - 18" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT", @@ -9937,20 +9163,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dev": true, - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/web-streams-polyfill": { "version": "3.2.1", "license": "MIT", @@ -9958,64 +9170,6 @@ "node": ">= 8" } }, - "node_modules/webpack": { - "version": "5.86.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.86.0.tgz", - "integrity": "sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/well-known-symbols": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", @@ -10168,6 +9322,7 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "devOptional": true, "license": "ISC" }, "node_modules/yallist": { @@ -10182,16 +9337,6 @@ "node": ">= 6" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "peer": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", @@ -10205,15 +9350,23 @@ } }, "node_modules/zustand": { - "version": "3.7.2", - "license": "MIT", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, "engines": { "node": ">=12.7.0" }, "peerDependencies": { + "immer": ">=9.0", "react": ">=16.8" }, "peerDependenciesMeta": { + "immer": { + "optional": true + }, "react": { "optional": true } diff --git a/apps/web/package.json b/apps/web/package.json index 51bd1e35f..b3686a358 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -67,11 +67,13 @@ "w3c-keyname": "^2.2.6", "web-streams-polyfill": "^3.1.1", "wouter": "2.7.3", - "zustand": "^3.3.1" + "zustand": "^4.3.8" }, "devDependencies": { + "@babel/core": "^7.22.5", "@playwright/test": "^1.35.0", "@trpc/server": "^10.29.1", + "@types/babel__core": "^7.20.1", "@types/file-saver": "^2.0.5", "@types/marked": "^4.0.7", "@types/node-fetch": "^2.5.10", @@ -79,6 +81,8 @@ "@types/react": "17.0.2", "@types/react-dom": "17.0.2", "@types/react-modal": "^3.13.1", + "@vitejs/plugin-react": "^4.0.0", + "@vitejs/plugin-react-refresh": "^1.3.6", "@vitejs/plugin-react-swc": "^3.3.2", "buffer": "^6.0.3", "chalk": "^4.1.0", diff --git a/apps/web/src/app-effects.jsx b/apps/web/src/app-effects.jsx index f312e3176..2ce039e6f 100644 --- a/apps/web/src/app-effects.jsx +++ b/apps/web/src/app-effects.jsx @@ -216,7 +216,7 @@ export default function AppEffects({ setShow }) { }) || {}; return () => { - unsubscribe(); + unsubscribe?.(); }; }, []); diff --git a/apps/web/src/common/db.js b/apps/web/src/common/db.ts similarity index 74% rename from apps/web/src/common/db.js rename to apps/web/src/common/db.ts index 9e4be5324..94cfae14c 100644 --- a/apps/web/src/common/db.js +++ b/apps/web/src/common/db.ts @@ -18,14 +18,14 @@ along with this program. If not, see . */ import { EventSourcePolyfill as EventSource } from "event-source-polyfill"; -import { NNStorage } from "../interfaces/storage"; +import { DatabasePersistence, NNStorage } from "../interfaces/storage"; import { logger } from "../utils/logger"; +import type Database from "@notesnook/core/api"; -/** - * @type {import("@notesnook/core/api/index").default} - */ -var db; -async function initializeDatabase(persistence) { +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +let db: Database = {}; +async function initializeDatabase(persistence: DatabasePersistence) { logger.measure("Database initialization"); const { default: Database } = await import("@notesnook/core/api"); @@ -33,8 +33,10 @@ async function initializeDatabase(persistence) { const { Compressor } = await import("../utils/compressor"); db = new Database( new NNStorage("Notesnook", persistence), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore EventSource, - FS, + null, new Compressor() ); @@ -42,7 +44,9 @@ async function initializeDatabase(persistence) { db.host({ API_HOST: "https://api.notesnook.com", AUTH_HOST: "https://auth.streetwriters.co", - SSE_HOST: "https://events.streetwriters.co" + SSE_HOST: "https://events.streetwriters.co", + ISSUES_HOST: "https://issues.streetwriters.co", + SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co" }); // } else { // db.host({ @@ -50,13 +54,13 @@ async function initializeDatabase(persistence) { // AUTH_HOST: "http://localhost:8264", // SSE_HOST: "http://localhost:7264", // }); - // const base = `http://${import.meta.env.REACT_APP_LOCALHOST}`; + // const base = `http://localhost`; // db.host({ // API_HOST: `${base}:5264`, // AUTH_HOST: `${base}:8264`, // SSE_HOST: `${base}:7264`, // ISSUES_HOST: `${base}:2624`, - // SUBSCRIPTIONS_HOST: `${base}:9264`, + // SUBSCRIPTIONS_HOST: `${base}:9264` // }); // } @@ -68,10 +72,11 @@ async function initializeDatabase(persistence) { logger.measure("Database initialization"); - if (db.migrations.required()) { + if (db.migrations?.required()) { const { showMigrationDialog } = await import("./dialog-controller"); await showMigrationDialog(); } + return db; } diff --git a/apps/web/src/common/desktop-bridge/index.ts b/apps/web/src/common/desktop-bridge/index.ts index abe51a8d8..f449b1242 100644 --- a/apps/web/src/common/desktop-bridge/index.ts +++ b/apps/web/src/common/desktop-bridge/index.ts @@ -21,5 +21,9 @@ import { type desktop as bridge } from "./bridge"; export const desktop: typeof bridge | undefined = import.meta.env.REACT_APP_PLATFORM === "desktop" - ? require("./bridge").desktop + ? process.env.NODE_ENV === "development" + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (await import("./bridge")).desktop + : require("./bridge.ts").desktop : undefined; diff --git a/apps/web/src/common/dialog-controller.tsx b/apps/web/src/common/dialog-controller.tsx index 5f7f07834..e17702b5b 100644 --- a/apps/web/src/common/dialog-controller.tsx +++ b/apps/web/src/common/dialog-controller.tsx @@ -116,7 +116,7 @@ export function showAddNotebookDialog() { export function showEditNotebookDialog(notebookId: string) { const notebook = db.notebooks?.notebook(notebookId)?.data; - if (!notebook) return false; + if (!notebook) return; return showDialog("AddNotebookDialog", (Dialog, perform) => ( ( - "EmailChangeDialog", - (Dialog, perform) => perform(null)} /> - ); -} - -export function showLanguageSelectorDialog() { - return showDialog<"LanguageSelectorDialog", string | null>( - "LanguageSelectorDialog", - (Dialog, perform) => perform(null)} /> - ); + return showDialog("EmailChangeDialog", (Dialog, perform) => ( + perform(null)} /> + )); } export function showToolbarConfigDialog() { - return showDialog<"ToolbarConfigDialog", string | null>( - "ToolbarConfigDialog", - (Dialog, perform) => perform(null)} /> - ); + return showDialog("ToolbarConfigDialog", (Dialog, perform) => ( + perform(null)} /> + )); } export function showError(title: string, message: string) { @@ -337,12 +328,6 @@ export function showMoveNoteDialog(noteIds: string[]) { )); } -export function showBillingHistoryDialog() { - return showDialog("BillingHistoryDialog", (Dialog, perform) => ( - perform(res)} /> - )); -} - function getDialogData(type: string) { switch (type) { case "create_vault": @@ -451,7 +436,7 @@ function getDialogData(type: string) { export function showPasswordDialog( type: string, - validate: (password: string) => boolean + validate: (password: string) => boolean | Promise ) { const { title, subtitle, positiveButtonText, checks } = getDialogData(type); return showDialog("PasswordDialog", (Dialog, perform) => ( @@ -485,6 +470,7 @@ export function showCreateTopicDialog() { onAction={async (topic: Record) => { if (!topic) return; const notebook = notebookStore.get().selectedNotebook; + if (!notebook) return; await db.notebooks?.notebook(notebook.id).topics.add(topic); notebookStore.setSelectedNotebook(notebook.id); showToast("success", "Topic created!"); @@ -613,12 +599,6 @@ export function showReminderPreviewDialog(reminder: Reminder) { )); } -export function showTrackingDetailsDialog() { - return showDialog("TrackingDetailsDialog", (Dialog, perform) => ( - perform(res)} /> - )); -} - export function showAddReminderDialog(noteId?: string) { return showDialog("AddReminderDialog", (Dialog, perform) => ( perform(res)} noteId={noteId} /> @@ -650,12 +630,6 @@ export function showIssueDialog() { )); } -export function showImportDialog() { - return showDialog("ImportDialog", (Dialog, perform) => ( - perform(res)} /> - )); -} - export function showMultifactorDialog(primaryMethod?: AuthenticatorType) { return showDialog("MultifactorDialog", (Dialog, perform) => ( perform(res)} primaryMethod={primaryMethod} /> @@ -674,6 +648,12 @@ export function showAttachmentsDialog() { )); } +export function showSettings() { + return showDialog("SettingsDialog", (Dialog, perform) => ( + perform(res)} /> + )); +} + export function showOnboardingDialog(type: string) { if (!type) return; return showDialog("OnboardingDialog", (Dialog, perform) => ( diff --git a/apps/web/src/common/store.js b/apps/web/src/common/store.ts similarity index 51% rename from apps/web/src/common/store.js rename to apps/web/src/common/store.ts index 5715006f2..92fbe91ab 100644 --- a/apps/web/src/common/store.js +++ b/apps/web/src/common/store.ts @@ -16,34 +16,30 @@ 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 . */ - -import produce, { immerable, setAutoFreeze } from "immer"; -import create from "zustand"; +import { immerable, setAutoFreeze } from "immer"; +import { create } from "zustand"; +import { subscribeWithSelector } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; +import { IStore } from "../stores"; setAutoFreeze(false); -function immer(config) { - return function (set, get, api) { - const obj = config( - (fn) => - set(() => - produce(get(), (state) => { - fn(state); - }) - ), - get, - api - ); - obj[immerable] = true; - return obj; - }; -} - -/** - * @returns {[import("zustand").UseStore, any]} - */ -function createStore(store) { - const useStore = create(immer(store.new.bind(store))); - return [useStore, useStore.getState()]; +export function createStore(Store: IStore) { + const store = create< + T, + [["zustand/subscribeWithSelector", never], ["zustand/immer", never]] + >( + subscribeWithSelector( + immer((set, get) => { + const store = new Store(set, get); + (store as any)[immerable] = true; + return store; + }) + ) + ); + // return Object.defineProperty(store as CustomStore, "get", { + // get: () => store.getState() + // }); + return [store, store.getState()] as const; } export default createStore; diff --git a/apps/web/src/common/task-manager.ts b/apps/web/src/common/task-manager.ts index 2c5aca4d0..80b10f9ef 100644 --- a/apps/web/src/common/task-manager.ts +++ b/apps/web/src/common/task-manager.ts @@ -55,7 +55,7 @@ type TaskProgress = { type ProgressReportCallback = (progress: TaskProgress) => void; export class TaskManager { - static async startTask(task: TaskDefinition): Promise { + static async startTask(task: TaskDefinition): Promise { switch (task.type) { case "status": { const statusTask = task; diff --git a/apps/web/src/components/cached-router/index.jsx b/apps/web/src/components/cached-router/index.jsx index 932ddca1a..a97328a1b 100644 --- a/apps/web/src/components/cached-router/index.jsx +++ b/apps/web/src/components/cached-router/index.jsx @@ -88,8 +88,4 @@ function CachedRouter() { ); } -export function clearRouteCache() { - cache = {}; -} - export default CachedRouter; diff --git a/apps/web/src/components/dialogs/billing-history-dialog.tsx b/apps/web/src/components/dialogs/billing-history-dialog.tsx deleted file mode 100644 index d61cadad8..000000000 --- a/apps/web/src/components/dialogs/billing-history-dialog.tsx +++ /dev/null @@ -1,145 +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 . -*/ - -import { useEffect, useState } from "react"; -import { Perform } from "../../common/dialog-controller"; -import Dialog from "./dialog"; -import { db } from "../../common/db"; -import { Loading } from "../icons"; -import { Flex, Link, Text } from "@theme-ui/components"; -import { getFormattedDate } from "../../utils/time"; - -type Transaction = { - order_id: string; - checkout_id: string; - amount: string; - currency: string; - status: keyof typeof TransactionStatusToText; - created_at: string; - passthrough: null; - product_id: number; - is_subscription: boolean; - is_one_off: boolean; - subscription: Subscription; - user: User; - receipt_url: string; -}; - -type Subscription = { - subscription_id: number; - status: string; -}; - -type User = { - user_id: number; - email: string; - marketing_consent: boolean; -}; - -const TransactionStatusToText = { - completed: "Completed", - refunded: "Refunded", - partially_refunded: "Partially refunded", - disputed: "Disputed" -}; - -export type BillingHistoryDialogProps = { - onClose: Perform; -}; - -export default function BillingHistoryDialog(props: BillingHistoryDialogProps) { - const [transactions, setTransactions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(); - - useEffect(() => { - (async function () { - try { - setError(undefined); - setIsLoading(true); - - const transactions = await db.subscriptions?.transactions(); - if (!transactions) return; - setTransactions(transactions); - } catch (e) { - if (e instanceof Error) setError(e); - } finally { - setIsLoading(false); - } - })(); - }, []); - - return ( - props.onClose(false)} - negativeButton={{ text: "Close", onClick: () => props.onClose(false) }} - width={600} - > - {isLoading ? ( - - ) : error ? ( - - - {error.message} -
- {error.stack} -
-
- ) : ( - - {transactions.map((transaction) => ( - - - Order #{transaction.order_id} - - {getFormattedDate(transaction.created_at, "date")} •{" "} - {TransactionStatusToText[transaction.status]} - - - - - {transaction.amount} {transaction.currency} - - - View receipt - - - - ))} - - )} -
- ); -} diff --git a/apps/web/src/components/dialogs/buy-dialog/buy-dialog.tsx b/apps/web/src/components/dialogs/buy-dialog/buy-dialog.tsx index 6109c806b..653d8f0f8 100644 --- a/apps/web/src/components/dialogs/buy-dialog/buy-dialog.tsx +++ b/apps/web/src/components/dialogs/buy-dialog/buy-dialog.tsx @@ -286,7 +286,7 @@ function TrialOrUpgrade(props: TrialOrUpgradeProps) { return ( <> - + Notesnook Pro @@ -377,7 +377,7 @@ function AlreadyPremium(props: AlreadyPremiumProps) { return ( <> - + Notesnook Pro @@ -411,7 +411,7 @@ function CheckoutCompleted(props: { onClose: () => void }) { return ( <> - + Thank you! @@ -478,9 +478,9 @@ function SelectedPlan(props: SelectedPlanProps) { return ( <> {plan.period === "monthly" ? ( - + ) : ( - + )} Notesnook Pro diff --git a/apps/web/src/components/dialogs/buy-dialog/features.tsx b/apps/web/src/components/dialogs/buy-dialog/features.tsx index ec752d2bb..152638fbd 100644 --- a/apps/web/src/components/dialogs/buy-dialog/features.tsx +++ b/apps/web/src/components/dialogs/buy-dialog/features.tsx @@ -437,21 +437,17 @@ export function Features() { flex: 1, flexDirection: "column", flexShrink: 0, - overflowY: ["hidden", "hidden", "auto"] + overflowY: ["hidden", "hidden", "auto"], + gap: 50 }} - pt={6} + pt={4} bg="background" > {sections.map((section) => { if (section.isVisible && !section.isVisible()) return null; return ( - + {section.pro && ( - + Choose a plan diff --git a/apps/web/src/components/dialogs/buy-dialog/store.ts b/apps/web/src/components/dialogs/buy-dialog/store.ts index 78867b8ad..890a62229 100644 --- a/apps/web/src/components/dialogs/buy-dialog/store.ts +++ b/apps/web/src/components/dialogs/buy-dialog/store.ts @@ -18,7 +18,7 @@ along with this program. If not, see . */ import { Plan, PricingInfo } from "./types"; -import create from "zustand"; +import { create } from "zustand"; import produce from "immer"; interface ICheckoutStore { diff --git a/apps/web/src/components/dialogs/import-dialog.tsx b/apps/web/src/components/dialogs/import-dialog.tsx deleted file mode 100644 index b7aa4ad49..000000000 --- a/apps/web/src/components/dialogs/import-dialog.tsx +++ /dev/null @@ -1,245 +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 . -*/ - -import { useCallback, useRef, useState } from "react"; -import { Flex, Text } from "@theme-ui/components"; -import Dialog from "./dialog"; -import { db } from "../../common/db"; -import { useDropzone } from "react-dropzone"; -import { Input } from "@theme-ui/components"; -import Accordion from "../accordion"; -import { store as appStore } from "../../stores/app-store"; -import { importFiles } from "../../utils/importer"; -import { Perform } from "../../common/dialog-controller"; -import { pluralize } from "../../utils/string"; - -type ImportDialogProps = { - onClose: Perform; -}; -function ImportDialog(props: ImportDialogProps) { - const [isDone, setIsDone] = useState(false); - const [isImporting, setIsImporting] = useState(false); - const [files, setFiles] = useState([]); - const [errors, setErrors] = useState([]); - const notesCounter = useRef(null); - const importProgress = useRef(null); - - const onDrop = useCallback((acceptedFiles) => { - setFiles((files) => { - const newFiles = [...acceptedFiles, ...files]; - return newFiles; - }); - }, []); - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, - accept: [".zip"] - }); - - return ( - props.onClose(false) - } - } - positiveButton={ - isDone - ? { - onClick: () => props.onClose(true), - text: "Close" - } - : { - onClick: async () => { - setIsDone(false); - setIsImporting(true); - - await db.syncer?.acquireLock(async () => { - try { - for await (const { - count, - filesRead, - totalFiles - } of importFiles(files)) { - if (notesCounter.current) - notesCounter.current.innerText = `${count}`; - if (importProgress.current) - importProgress.current.style.width = `${ - (filesRead / totalFiles) * 100 - }%`; - } - } catch (e) { - console.error(e); - if (e instanceof Error) { - setErrors((errors) => [...errors, e as Error]); - } - } - }); - - await appStore.refresh(); - - setIsDone(true); - setIsImporting(false); - }, - text: - files.length > 0 - ? `Import notes from ${pluralize( - files.length, - "file", - "files" - )}` - : "Import", - loading: isImporting, - disabled: files.length <= 0 || isImporting - } - } - > - - {isImporting ? ( - <> - - 0 notes imported. - - - - - ) : isDone ? ( - <> - - Import successful. {errors.length} errors occured. - - {errors.length > 0 && ( - - {errors.map((error) => ( - - {error.message} - - ))} - - )} - - ) : ( - <> - - - - - {isDragActive - ? "Drop the files here" - : "Drag & drop files here, or click to select files"} -
- Only .zip files are supported. -
-
- - {files.map((file, i) => ( - { - setFiles((files) => { - const cloned = files.slice(); - cloned.splice(i, 1); - return cloned; - }); - }} - variant="body" - ml={1} - title="Click to remove" - > - {file.name} - - ))} - -
- - - - Go to{" "} - - https://importer.notesnook.com/ - - - - Select the notes app you want to import from - - - Follow the next steps in the Importer to download the .zip - file - - - Drag & drop the downloaded .zip file above - - - - - )} -
-
- ); -} -export default ImportDialog; diff --git a/apps/web/src/components/dialogs/index.ts b/apps/web/src/components/dialogs/index.ts index 9eb3d8714..c51161902 100644 --- a/apps/web/src/components/dialogs/index.ts +++ b/apps/web/src/components/dialogs/index.ts @@ -25,7 +25,6 @@ const Confirm = React.lazy(() => import("./confirm")); const EmailVerificationDialog = React.lazy( () => import("./email-verification-dialog") ); -const ImportDialog = React.lazy(() => import("./import-dialog")); const LoadingDialog = React.lazy(() => import("./loading-dialog")); const ProgressDialog = React.lazy(() => import("./progress-dialog")); const MoveDialog = React.lazy(() => import("./move-note-dialog")); @@ -33,9 +32,6 @@ const PasswordDialog = React.lazy(() => import("./password-dialog")); const RecoveryKeyDialog = React.lazy(() => import("./recovery-key-dialog")); const ItemDialog = React.lazy(() => import("./item-dialog")); const FeatureDialog = React.lazy(() => import("./feature-dialog")); -const TrackingDetailsDialog = React.lazy( - () => import("./tracking-details-dialog.jsx") -); const ReminderDialog = React.lazy(() => import("./reminder-dialog")); const AddReminderDialog = React.lazy(() => import("./add-reminder-dialog")); const ReminderPreviewDialog = React.lazy( @@ -54,18 +50,12 @@ const Prompt = React.lazy(() => import("./prompt")); const ToolbarConfigDialog = React.lazy(() => import("./toolbar-config-dialog")); const MigrationDialog = React.lazy(() => import("./migration-dialog")); const EmailChangeDialog = React.lazy(() => import("./email-change-dialog")); -const LanguageSelectorDialog = React.lazy( - () => import("./language-selector-dialog") -); -const BillingHistoryDialog = React.lazy( - () => import("./billing-history-dialog") -); const AddTagsDialog = React.lazy(() => import("./add-tags-dialog")); +const SettingsDialog = React.lazy(() => import("./settings")); export const Dialogs = { AddNotebookDialog, ToolbarConfigDialog, - TrackingDetailsDialog, BuyDialog, Confirm, Prompt, @@ -80,7 +70,6 @@ export const Dialogs = { ReminderDialog, AnnouncementDialog, IssueDialog, - ImportDialog, MultifactorDialog, RecoveryCodesDialog, OnboardingDialog, @@ -89,7 +78,6 @@ export const Dialogs = { AddReminderDialog, ReminderPreviewDialog, EmailChangeDialog, - LanguageSelectorDialog, - BillingHistoryDialog, - AddTagsDialog + AddTagsDialog, + SettingsDialog }; diff --git a/apps/web/src/components/dialogs/language-selector-dialog.tsx b/apps/web/src/components/dialogs/language-selector-dialog.tsx deleted file mode 100644 index f76b5c8a1..000000000 --- a/apps/web/src/components/dialogs/language-selector-dialog.tsx +++ /dev/null @@ -1,104 +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 . -*/ - -import { deleteItem } from "@notesnook/core/utils/array"; -import { Input, Label } from "@theme-ui/components"; -import { useCallback, useEffect, useState } from "react"; -import { Perform } from "../../common/dialog-controller"; -import useSpellChecker, { Language } from "../../hooks/use-spell-checker"; -import { FlexScrollContainer } from "../scroll-container"; -import Dialog from "./dialog"; - -export type LanguageSelectorDialogProps = { - onClose: Perform; -}; - -export default function LanguageSelectorDialog( - props: LanguageSelectorDialogProps -) { - const spellChecker = useSpellChecker(); - const [enabledLanguages, setEnabledLanguages] = useState([]); - const [languages, setLanguages] = useState([]); - - useEffect(() => { - if (!spellChecker.enabledLanguages || !spellChecker.languages) return; - - setEnabledLanguages(spellChecker.enabledLanguages.map((a) => a.code)); - setLanguages(spellChecker.languages.slice()); - }, [spellChecker.enabledLanguages, spellChecker.languages]); - - const filter = useCallback( - async (query) => { - if (!spellChecker.languages) return; - setLanguages( - spellChecker.languages.filter( - (a) => - a.name.toLowerCase().includes(query) || - a.code.toLowerCase().includes(query) - ) - ); - }, - [spellChecker] - ); - - return ( - props.onClose(false)} - positiveButton={{ - text: "Done", - onClick: () => props.onClose(true) - }} - negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }} - > - filter(e.currentTarget.value)} - /> - - {languages.map((lang) => ( - - ))} - - - ); -} diff --git a/apps/web/src/components/dialogs/move-note-dialog.tsx b/apps/web/src/components/dialogs/move-note-dialog.tsx index f72b0c319..7f1b086f8 100644 --- a/apps/web/src/components/dialogs/move-note-dialog.tsx +++ b/apps/web/src/components/dialogs/move-note-dialog.tsx @@ -28,7 +28,7 @@ import { Perform } from "../../common/dialog-controller"; import { showToast } from "../../utils/toast"; import { pluralize } from "../../utils/string"; import { isMac } from "../../utils/platform"; -import create from "zustand"; +import { create } from "zustand"; import { FilteredList } from "../filtered-list"; type MoveDialogProps = { onClose: Perform; noteIds: string[] }; diff --git a/apps/web/src/components/dialogs/settings/appearance-settings.ts b/apps/web/src/components/dialogs/settings/appearance-settings.ts new file mode 100644 index 000000000..054f4c233 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/appearance-settings.ts @@ -0,0 +1,94 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; +import { useStore as useThemeStore } from "../../../stores/theme-store"; +import { isDesktop } from "../../../utils/platform"; +import { AccentColors } from "./components/accent-colors"; + +export const AppearanceSettings: SettingsGroup[] = [ + { + key: "theme", + section: "appearance", + header: "Theme", + settings: [ + { + key: "accent-color", + title: "Accent color", + description: "Pick the color that matches your mood.", + components: [ + { + type: "custom", + component: AccentColors + } + ] + }, + { + key: "theme", + title: "App theme", + description: "Dark or light, we won't judge.", + onStateChange: (listener) => + useThemeStore.subscribe( + (s) => [s.theme, s.followSystemTheme], + listener + ), + components: [ + { + type: "dropdown", + options: [ + { title: "Light", value: "light" }, + { title: "Dark", value: "dark" }, + { title: "Auto", value: "auto" } + ], + selectedOption: () => + useThemeStore.getState().followSystemTheme + ? "auto" + : useThemeStore.getState().theme, + onSelectionChanged: (value) => { + useThemeStore.getState().setFollowSystemTheme(value === "auto"); + if (value !== "auto") useThemeStore.getState().setTheme(value); + } + } + ] + }, + { + key: "zoom-factor", + title: "Zoom factor", + description: "Zoom in or out the app content.", + isHidden: () => !isDesktop(), + onStateChange: (listener) => + useThemeStore.subscribe( + (s) => [s.theme, s.followSystemTheme], + listener + ), + components: [ + { + type: "input", + inputType: "number", + min: 0.5, + max: 2.0, + defaultValue: () => useSettingStore.getState().zoomFactor, + onChange: (value) => useSettingStore.getState().setZoomFactor(value) + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/auth-settings.ts b/apps/web/src/components/dialogs/settings/auth-settings.ts new file mode 100644 index 000000000..978d9b61b --- /dev/null +++ b/apps/web/src/components/dialogs/settings/auth-settings.ts @@ -0,0 +1,112 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useUserStore } from "../../../stores/user-store"; +import { verifyAccount } from "../../../common"; +import { + show2FARecoveryCodesDialog, + showMultifactorDialog +} from "../../../common/dialog-controller"; + +export const AuthenticationSettings: SettingsGroup[] = [ + { + header: "Password", + key: "password", + section: "auth", + settings: [ + { + key: "change-password", + title: "Change password", + description: "Set a new password for your account", + keywords: ["change password", "new password"], + components: [ + { + type: "button", + title: "Change password", + variant: "secondary", + action: () => {} + } + ] + } + ] + }, + { + header: "Two-factor authentication", + key: "2fa", + section: "auth", + settings: [ + { + key: "2fa-enabled", + title: "2FA enabled", + description: "Your account is secured by 2FA.", + keywords: [], + components: [] + }, + { + key: "fallback-2fa-method", + title: "Fallback method", + description: + "You can use the fallback 2FA method in case you are unable to login via the primary method.", + keywords: ["backup 2fa method"], + onStateChange: (listener) => + useUserStore.subscribe((s) => s.user?.mfa.secondaryMethod, listener), + components: () => [ + { + type: "button", + title: useUserStore.getState().user?.mfa.secondaryMethod + ? "Reconfigure fallback 2FA method" + : "Add fallback 2FA method", + variant: "secondary", + action: async () => { + if (await verifyAccount()) { + await showMultifactorDialog( + useUserStore.getState().user?.mfa.primaryMethod || "email" + ); + await useUserStore.getState().refreshUser(); + } + } + } + ] + }, + { + key: "recovery-codes", + title: "Recovery codes", + description: + "Recovery codes can be used to login in case you cannot use any of the other 2FA methods.", + keywords: ["2fa recovery codes"], + components: [ + { + type: "button", + title: "View recovery codes", + variant: "secondary", + action: async () => { + if (await verifyAccount()) { + await show2FARecoveryCodesDialog( + useUserStore.getState().user?.mfa.primaryMethod || "email" + ); + await useUserStore.getState().refreshUser(); + } + } + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/backup-export-settings.ts b/apps/web/src/components/dialogs/settings/backup-export-settings.ts new file mode 100644 index 000000000..80081e9ce --- /dev/null +++ b/apps/web/src/components/dialogs/settings/backup-export-settings.ts @@ -0,0 +1,175 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; +import { useStore as useAppStore } from "../../../stores/app-store"; +import { useStore as useUserStore } from "../../../stores/user-store"; +import { isUserPremium } from "../../../hooks/use-is-user-premium"; +import { createBackup, importBackup, verifyAccount } from "../../../common"; +import { db } from "../../../common/db"; +import { exportNotes } from "../../../common/export"; +import { isDesktop, isTesting } from "../../../utils/platform"; +import { desktop } from "../../../common/desktop-bridge"; +import { PATHS } from "@notesnook/desktop"; + +export const BackupExportSettings: SettingsGroup[] = [ + { + key: "backup", + section: "backup-export", + header: "Backups", + settings: [ + { + key: "create-backup", + title: "Backup now", + description: "Create a backup file containing all your data", + components: [ + { + type: "button", + title: "Create backup", + action: async () => { + if (!isUserPremium() && useSettingStore.getState().encryptBackups) + useSettingStore.getState().toggleEncryptBackups(); + if (await verifyAccount()) await createBackup(); + }, + variant: "secondary" + } + ] + }, + { + key: "restore-backup", + title: "Restore backup", + description: "Restore a backup file from your disk drive.", + isHidden: () => !useUserStore.getState().isLoggedIn && !isTesting(), + components: [ + { + type: "button", + title: "Restore", + action: async () => { + await importBackup(); + await useAppStore.getState().refresh(); + }, + variant: "secondary" + } + ] + }, + { + key: "auto-backup", + title: isDesktop() ? "Automatic backups" : "Backup reminders", + description: isDesktop() + ? "Backup all your data automatically at a set interval." + : "You will be shown regular reminders to backup your data.", + isHidden: () => !isUserPremium(), + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.backupReminderOffset, listener), + components: [ + { + type: "dropdown", + options: [ + { value: "0", title: "Never" }, + { value: "1", title: "Daily" }, + { value: "2", title: "Weekly" }, + { value: "3", title: "Monthly" } + ], + selectedOption: () => + useSettingStore.getState().backupReminderOffset.toString(), + onSelectionChanged: (value) => + useSettingStore + .getState() + .setBackupReminderOffset(parseInt(value)) + } + ] + }, + { + key: "encrypt-backups", + title: "Backup encryption", + description: "Encrypt all backup files using your master key.", + isHidden: () => !isUserPremium(), + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.encryptBackups, listener), + components: [ + { + type: "toggle", + isToggled: () => useSettingStore.getState().encryptBackups, + toggle: () => useSettingStore.getState().toggleEncryptBackups() + } + ] + }, + { + key: "backup-directory", + title: "Backups directory", + description: "Select directory to store all backup files.", + isHidden: () => !isDesktop(), + components: [ + { + type: "button", + title: "Select directory", + action: async () => { + const backupStorageLocation = + useSettingStore.getState().backupStorageLocation || + PATHS.backupsDirectory; + const location = await desktop?.integration.selectDirectory.query( + { + title: "Select where Notesnook should save backups", + defaultPath: backupStorageLocation + } + ); + if (!location) return; + useSettingStore.getState().setBackupStorageLocation(location); + }, + variant: "secondary" + } + ] + } + ] + }, + { + key: "export", + section: "backup-export", + header: "Export", + settings: [ + { + key: "export-notes", + title: "Export all notes", + description: "Export all notes as Markdown, HTML or Plaintext.", + components: [ + { + type: "dropdown", + options: [ + { value: "-", title: "" }, + { value: "txt", title: "Text" }, + { value: "md", title: "Markdown", premium: true }, + { value: "html", title: "HTML", premium: true } + ], + selectedOption: () => "-", + onSelectionChanged: async (value) => { + if (!db.notes || value === "-") return; + + await db.notes.init(); + await exportNotes( + value as "txt" | "md" | "html", + db.notes.all.map((n) => n.id) + ); + } + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/behaviour-settings.ts b/apps/web/src/components/dialogs/settings/behaviour-settings.ts new file mode 100644 index 000000000..d4701f3fa --- /dev/null +++ b/apps/web/src/components/dialogs/settings/behaviour-settings.ts @@ -0,0 +1,138 @@ +/* +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 . +*/ + +import { DATE_FORMATS } from "@notesnook/core/common"; +import { SettingsGroup } from "./types"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; +import dayjs from "dayjs"; +import { isUserPremium } from "../../../hooks/use-is-user-premium"; + +export const BehaviourSettings: SettingsGroup[] = [ + { + key: "general", + section: "behaviour", + header: "General", + settings: [ + { + key: "default-homepage", + title: "Home page", + description: "Default screen to open on app startup.", + keywords: ["welcome page", "default screen"], + isHidden: () => !isUserPremium(), + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.homepage, listener), + components: [ + { + type: "dropdown", + onSelectionChanged: (value) => + useSettingStore.getState().setHomepage(parseInt(value)), + selectedOption: () => + useSettingStore.getState().homepage.toString(), + options: [ + { value: "0", title: "Notes" }, + { value: "1", title: "Notebooks" }, + { value: "2", title: "Favorites" }, + { value: "3", title: "Tags" } + ] + } + ] + } + ] + }, + { + key: "date-time", + section: "behaviour", + header: "Date & time", + settings: [ + { + key: "date-format", + title: "Date format", + description: "This date format will be used everywhere in the app.", + keywords: [], + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.dateFormat, listener), + components: [ + { + type: "dropdown", + onSelectionChanged: (value) => + useSettingStore.getState().setDateFormat(value), + selectedOption: () => useSettingStore.getState().dateFormat, + options: DATE_FORMATS.map((a) => ({ + value: a, + title: `${a} (${dayjs(Date.now()).format(a)})` + })) + } + ] + }, + { + key: "time-format", + title: "Time format", + description: "This time format will be used everywhere in the app.", + keywords: [], + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.timeFormat, listener), + components: [ + { + type: "dropdown", + onSelectionChanged: (value) => + useSettingStore.getState().setTimeFormat(value), + selectedOption: () => useSettingStore.getState().timeFormat, + options: [ + { value: "12-hour", title: "12h" }, + { value: "24-hour", title: "24h" } + ] + } + ] + } + ] + }, + { + key: "trash", + section: "behaviour", + header: "Trash", + settings: [ + { + key: "trash-cleanup-interval", + title: "Cleanup interval", + description: + "All items in the trash will be permanently deleted after this interval.", + keywords: ["clear trash", "trash cleanup interval"], + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.trashCleanupInterval, listener), + components: [ + { + type: "dropdown", + onSelectionChanged: (value) => + useSettingStore + .getState() + .setTrashCleanupInterval(parseInt(value)), + selectedOption: () => + useSettingStore.getState().trashCleanupInterval.toString(), + options: [ + { value: "7", title: "Weekly" }, + { value: "30", title: "Monthly" }, + { value: "365", title: "Yearly" }, + { value: "-1", title: "Never", premium: true } + ] + } + ] + } + ] + } +]; diff --git a/apps/web/src/hooks/use-zoom-factor.ts b/apps/web/src/components/dialogs/settings/components/accent-colors.tsx similarity index 59% rename from apps/web/src/hooks/use-zoom-factor.ts rename to apps/web/src/components/dialogs/settings/components/accent-colors.tsx index 10d4df5a9..e874f7c9c 100644 --- a/apps/web/src/hooks/use-zoom-factor.ts +++ b/apps/web/src/components/dialogs/settings/components/accent-colors.tsx @@ -17,22 +17,23 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { useCallback, useEffect, useState } from "react"; -import { desktop } from "../common/desktop-bridge"; +import { getAllAccents } from "@notesnook/theme"; +import { Flex } from "@theme-ui/components"; +import AccentItem from "../../../accent-item"; -export default function useZoomFactor() { - const [zoom, setZoom] = useState(1.0); - - useEffect(() => { - (async function () { - setZoom((await desktop?.integration.zoomFactor.query()) || 1.0); - })(); - }, []); - - const set = useCallback(async (zoomFactor) => { - await desktop?.integration.setZoomFactor.mutate(zoomFactor); - setZoom(zoomFactor); - }, []); - - return [zoom, set]; +export function AccentColors() { + return ( + + {getAllAccents().map((color) => ( + + ))} + + ); } diff --git a/apps/web/src/components/dialogs/settings/components/billing-history.tsx b/apps/web/src/components/dialogs/settings/components/billing-history.tsx new file mode 100644 index 000000000..c3445f19b --- /dev/null +++ b/apps/web/src/components/dialogs/settings/components/billing-history.tsx @@ -0,0 +1,197 @@ +/* +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 . +*/ + +import { useEffect, useState } from "react"; +import { Loading } from "../../../icons"; +import { Box, Flex, Link, Text } from "@theme-ui/components"; +import { getFormattedDate } from "../../../../utils/time"; +import { db } from "../../../../common/db"; + +type Transaction = { + order_id: string; + checkout_id: string; + amount: string; + currency: string; + status: keyof typeof TransactionStatusToText; + created_at: string; + passthrough: null; + product_id: number; + is_subscription: boolean; + is_one_off: boolean; + subscription: Subscription; + user: User; + receipt_url: string; +}; + +type Subscription = { + subscription_id: number; + status: string; +}; + +type User = { + user_id: number; + email: string; + marketing_consent: boolean; +}; + +const TransactionStatusToText = { + completed: "Completed", + refunded: "Refunded", + partially_refunded: "Partially refunded", + disputed: "Disputed" +}; + +export function BillingHistory() { + const [transactions, setTransactions] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + + useEffect(() => { + (async function () { + try { + setError(undefined); + setIsLoading(true); + + const transactions = await db.subscriptions?.transactions(); + if (!transactions) return; + setTransactions(transactions); + } catch (e) { + if (e instanceof Error) setError(e); + } finally { + setIsLoading(false); + } + })(); + }, []); + + return ( + <> + {isLoading ? ( + + ) : error ? ( + + + {error.message} +
+ {error.stack} +
+
+ ) : ( + + + + {[ + { id: "date", title: "Date", width: "20%" }, + { id: "orderId", title: "Order ID", width: "20%" }, + { id: "amount", title: "Amount", width: "20%" }, + { id: "status", title: "Status", width: "20%" }, + { id: "receipt", title: "Receipt", width: "20%" } + ].map((column) => + !column.title ? ( + + + {transactions.map((transaction) => ( + + + {getFormattedDate(transaction.created_at, "date")} + + + {transaction.order_id} + + + {transaction.amount} {transaction.currency} + + + {TransactionStatusToText[transaction.status]} + + + + View receipt + + + + ))} + +
+ ) : ( + + + {column.title} + + + ) + )} + +
+ )} + + ); +} + +{ + /* + + + Order #{transaction.order_id} + + + + + {transaction.amount} {transaction.currency} + + + + + */ +} diff --git a/apps/web/src/components/dialogs/settings/components/importer.tsx b/apps/web/src/components/dialogs/settings/components/importer.tsx new file mode 100644 index 000000000..389b9275d --- /dev/null +++ b/apps/web/src/components/dialogs/settings/components/importer.tsx @@ -0,0 +1,218 @@ +/* +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 . +*/ + +import { useStore as useAppStore } from "../../../../stores/app-store"; +import { useCallback, useRef, useState } from "react"; +import { useDropzone } from "react-dropzone"; +import { Button, Flex, Input, Link, Text } from "@theme-ui/components"; +import { pluralize } from "../../../../utils/string"; +import { db } from "../../../../common/db"; +import { importFiles } from "../../../../utils/importer"; +import { CheckCircleOutline } from "../../../icons"; + +export function Importer() { + const [isDone, setIsDone] = useState(false); + const [isImporting, setIsImporting] = useState(false); + const [files, setFiles] = useState([]); + const [errors, setErrors] = useState([]); + const notesCounter = useRef(null); + const importProgress = useRef(null); + + const onDrop = useCallback((acceptedFiles) => { + setFiles((files) => { + const newFiles = [...acceptedFiles, ...files]; + return newFiles; + }); + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: [".zip"] + }); + + return ( + + {isImporting ? ( + <> + + 0 notes imported. + + + + + ) : isDone ? ( + <> + + + Import successful. {errors.length} errors occured. + + + {errors.length > 0 && ( + + {errors.map((error) => ( + + {error.message} + + ))} + + )} + + ) : ( + <> + + + + {files.length + ? `${pluralize( + files.length, + "file", + "files" + )} ready for import` + : "Select files to import"} + + + Please refer to the{" "} + + import guide + {" "} + for help regarding how to use the Notesnook Importer. + + + + + + + + {isDragActive + ? "Drop the files here" + : "Drag & drop files here, or click to select files"} +
+ Only .zip files are supported. +
+
+ + {files.map((file, i) => ( + { + setFiles((files) => { + const cloned = files.slice(); + cloned.splice(i, 1); + return cloned; + }); + }} + variant="body" + title="Click to remove" + > + {file.name} + + ))} + + + )} +
+ ); +} diff --git a/apps/web/src/components/dialogs/settings/components/spell-checker-languages.tsx b/apps/web/src/components/dialogs/settings/components/spell-checker-languages.tsx new file mode 100644 index 000000000..f6ca48b85 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/components/spell-checker-languages.tsx @@ -0,0 +1,78 @@ +/* +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 . +*/ + +import { Language, useSpellChecker } from "../../../../hooks/use-spell-checker"; +import { Checkbox, Input, Label } from "@theme-ui/components"; +import { useCallback, useEffect, useState } from "react"; +import { deleteItem } from "@notesnook/core/utils/array"; + +export function SpellCheckerLanguages() { + const spellChecker = useSpellChecker(); + const [enabledLanguages, setEnabledLanguages] = useState([]); + const [languages, setLanguages] = useState([]); + + useEffect(() => { + if (!spellChecker.enabledLanguages || !spellChecker.languages) return; + + setEnabledLanguages(spellChecker.enabledLanguages.map((a) => a.code)); + setLanguages(spellChecker.languages.slice()); + }, [spellChecker.enabledLanguages, spellChecker.languages]); + + const filter = useCallback( + async (query) => { + if (!spellChecker.languages) return; + setLanguages( + spellChecker.languages.filter( + (a) => + a.name.toLowerCase().includes(query) || + a.code.toLowerCase().includes(query) + ) + ); + }, + [spellChecker] + ); + + return ( + <> + filter(e.currentTarget.value.toLowerCase().trim())} + /> + {languages.map((lang) => ( + + ))} + + ); +} diff --git a/apps/web/src/components/dialogs/settings/components/subscription-status.tsx b/apps/web/src/components/dialogs/settings/components/subscription-status.tsx new file mode 100644 index 000000000..1c13522f2 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/components/subscription-status.tsx @@ -0,0 +1,254 @@ +/* +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 . +*/ + +import { useStore as useUserStore } from "../../../../stores/user-store"; +import { Button, Flex, Text } from "@theme-ui/components"; +import { useCallback, useMemo, useState } from "react"; +import dayjs from "dayjs"; +import { SUBSCRIPTION_STATUS } from "../../../../common/constants"; +import { db } from "../../../../common/db"; +import { confirm, showBuyDialog } from "../../../../common/dialog-controller"; +import { TaskManager } from "../../../../common/task-manager"; +import { showToast } from "../../../../utils/toast"; +import { Loading } from "../../../icons"; +import { Features } from "../../buy-dialog/features"; + +const PROVIDER_MAP = { + 0: "Streetwriters", + 1: "iOS", + 2: "Android", + 3: "Web" +} as const; +export function SubscriptionStatus() { + const user = useUserStore((store) => store.user); + + const [activateTrial, isActivatingTrial] = useAction(async () => { + await db.user?.activateTrial(); + }); + + const provider = PROVIDER_MAP[user?.subscription?.provider || 0]; + const { + isTrial, + isBeta, + isPro, + isBasic, + isProCancelled, + isProExpired, + remainingDays + } = useMemo(() => { + const type = user?.subscription?.type; + const expiry = user?.subscription?.expiry; + if (!expiry) return { isBasic: true, remainingDays: 0 }; + return { + remainingDays: dayjs(expiry).diff(dayjs(), "day"), + isTrial: type === SUBSCRIPTION_STATUS.TRIAL, + isBasic: type === SUBSCRIPTION_STATUS.BASIC, + isBeta: type === SUBSCRIPTION_STATUS.BETA, + isPro: type === SUBSCRIPTION_STATUS.PREMIUM, + isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED, + isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED + }; + }, [user]); + + const subtitle = useMemo(() => { + const expiryDate = dayjs(user?.subscription?.expiry).format("MMMM D, YYYY"); + const startDate = dayjs(user?.subscription?.start).format("MMMM D, YYYY"); + return isPro + ? provider === "Streetwriters" + ? `Ending on ${expiryDate}` + : `Next payment on ${expiryDate}.` + : isProCancelled + ? `Ending on ${expiryDate}.` + : isProExpired + ? "Your account will be downgraded to Basic in 3 days." + : isBeta + ? `Beta member since ${startDate}` + : isTrial + ? `Ending on ${expiryDate}` + : null; + }, [isPro, isProExpired, isProCancelled, isBeta, isTrial, user, provider]); + + if (!user) return null; + return ( + <> + + + CURRENT PLAN + + + {remainingDays > 0 && isPro + ? `Pro` + : remainingDays > 0 && isTrial + ? "Trial" + : isBeta + ? "Beta user" + : "Basic"} + + + {remainingDays > 0 && (isPro || isTrial || isBeta) + ? `Access to all Pro features including unlimited storage for attachments, + notebooks & tags.` + : "Access only to basic features including unlimited notes & end-to-end encrypted syncing to unlimited devices."} + + + {subtitle} + + + {/* */} + {provider === "Web" && isPro ? ( + <> + + + + ) : isBasic ? ( + <> + + + + ) : isTrial ? ( + <> + + + ) : null} + + + {isBasic ? : null} + + ); +} + +function useAction(action: () => Promise) { + const [isLoading, setIsLoading] = useState(false); + + const _action = useCallback(async () => { + try { + setIsLoading(true); + await action(); + } catch (e) { + if (e instanceof Error) { + showToast("error", e.message); + } + } finally { + setIsLoading(false); + } + }, [action]); + + return [_action, isLoading] as const; +} diff --git a/apps/web/src/components/dialogs/settings/components/tracking-details.tsx b/apps/web/src/components/dialogs/settings/components/tracking-details.tsx new file mode 100644 index 000000000..725bff2cd --- /dev/null +++ b/apps/web/src/components/dialogs/settings/components/tracking-details.tsx @@ -0,0 +1,85 @@ +/* +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 . +*/ + +import { Box, Text } from "@theme-ui/components"; +import { ANALYTICS_EVENTS } from "../../../../utils/analytics"; + +const events = Object.values(ANALYTICS_EVENTS); +export function TrackingDetails() { + return ( + + + + {[ + { id: "event-name", title: "Event name", width: "15%" }, + { id: "event-detail", title: "Detail", width: "50%" } + ].map((column) => + !column.title ? ( + + + {events.map((event) => ( + + + {event.name} + + + {event.description} + + + ))} + +
+ ) : ( + + + {column.title} + + + ) + )} + +
+ ); +} diff --git a/apps/web/src/components/dialogs/settings/components/user-profile.tsx b/apps/web/src/components/dialogs/settings/components/user-profile.tsx new file mode 100644 index 000000000..5c3ded230 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/components/user-profile.tsx @@ -0,0 +1,130 @@ +/* +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 . +*/ + +import { Flex, Text } from "@theme-ui/components"; +import { User } from "../../../icons"; +import { useStore as useUserStore } from "../../../../stores/user-store"; +import ObjectID from "@notesnook/core/utils/object-id"; +import { getFormattedDate } from "../../../../utils/time"; +import { SUBSCRIPTION_STATUS } from "../../../../common/constants"; +import dayjs from "dayjs"; +import { useMemo } from "react"; + +export function UserProfile() { + const user = useUserStore((store) => store.user); + + const { + isTrial, + isBeta, + isPro, + isBasic, + isProCancelled, + isProExpired, + remainingDays + } = useMemo(() => { + const type = user?.subscription?.type; + const expiry = user?.subscription?.expiry; + if (!expiry) return { isBasic: true, remainingDays: 0 }; + return { + remainingDays: dayjs(expiry).diff(dayjs(), "day"), + isTrial: type === SUBSCRIPTION_STATUS.TRIAL, + isBasic: type === SUBSCRIPTION_STATUS.BASIC, + isBeta: type === SUBSCRIPTION_STATUS.BETA, + isPro: type === SUBSCRIPTION_STATUS.PREMIUM, + isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED, + isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED + }; + }, [user]); + + if (!user) + return ( + + + + + + You are not logged in + + Login or create an account to sync your notes. + + + + ); + + return ( + + + + + + + {remainingDays > 0 && isPro + ? `PRO` + : remainingDays > 0 && isTrial + ? "TRIAL" + : isBeta + ? "BETA TESTER" + : "BASIC"} + + {user.email} + + Member since{" "} + {getFormattedDate(new ObjectID(user.id).getTimestamp(), "date")} + + + + ); +} diff --git a/apps/web/src/components/dialogs/settings/desktop-integration-settings.ts b/apps/web/src/components/dialogs/settings/desktop-integration-settings.ts new file mode 100644 index 000000000..0139010fc --- /dev/null +++ b/apps/web/src/components/dialogs/settings/desktop-integration-settings.ts @@ -0,0 +1,132 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; +import { type DesktopIntegrationSettings as DesktopIntegrationSettingsType } from "../../../hooks/use-desktop-integration"; + +export const DesktopIntegrationSettings: SettingsGroup[] = [ + { + key: "desktop-integration", + section: "desktop", + header: "Desktop integration", + settings: [ + { + key: "auto-start", + title: "Auto start on system startup", + description: + "If true, Notesnook will automatically start up when you turn on & login to your system.", + onStateChange: (listener) => + useSettingStore.subscribe( + (s) => s.desktopIntegrationSettings?.autoStart, + listener + ), + components: [ + { + type: "toggle", + isToggled: () => + !!useSettingStore.getState().desktopIntegrationSettings + ?.autoStart, + toggle: () => + useSettingStore.getState().setDesktopIntegration({ + autoStart: + !useSettingStore.getState().desktopIntegrationSettings + ?.autoStart + }) + } + ] + }, + { + key: "start-minimized", + title: "Start minimized", + description: + "If true, Notesnook will start minimized to either the system tray or your system taskbar/dock. This setting only works with Auto start on system startup is enabled.", + onStateChange: (listener) => + useSettingStore.subscribe( + (s) => s.desktopIntegrationSettings, + listener + ), + isHidden: (desktopIntegration) => + !(desktopIntegration as DesktopIntegrationSettingsType)?.autoStart, + components: [ + { + type: "toggle", + isToggled: () => + !!useSettingStore.getState().desktopIntegrationSettings + ?.startMinimized, + toggle: () => + useSettingStore.getState().setDesktopIntegration({ + startMinimized: + !useSettingStore.getState().desktopIntegrationSettings + ?.startMinimized + }) + } + ] + }, + { + key: "minimize-to-tray", + title: "Minimize to system tray", + description: 'Pressing "—" will hide the app in your system tray.', + onStateChange: (listener) => + useSettingStore.subscribe( + (s) => s.desktopIntegrationSettings?.minimizeToSystemTray, + listener + ), + components: [ + { + type: "toggle", + isToggled: () => + !!useSettingStore.getState().desktopIntegrationSettings + ?.minimizeToSystemTray, + toggle: () => + useSettingStore.getState().setDesktopIntegration({ + autoStart: + !useSettingStore.getState().desktopIntegrationSettings + ?.minimizeToSystemTray + }) + } + ] + }, + { + key: "close-to-tray", + title: "Close to system tray", + description: 'Pressing "X" will hide the app in your system tray.', + onStateChange: (listener) => + useSettingStore.subscribe( + (s) => s.desktopIntegrationSettings?.closeToSystemTray, + listener + ), + components: [ + { + type: "toggle", + isToggled: () => + !!useSettingStore.getState().desktopIntegrationSettings + ?.closeToSystemTray, + toggle: () => + useSettingStore.getState().setDesktopIntegration({ + autoStart: + !useSettingStore.getState().desktopIntegrationSettings + ?.closeToSystemTray + }) + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/editor-settings.ts b/apps/web/src/components/dialogs/settings/editor-settings.ts new file mode 100644 index 000000000..450858189 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/editor-settings.ts @@ -0,0 +1,154 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { + editorConfig, + onEditorConfigChange, + setEditorConfig +} from "../../editor/context"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; +import { getFonts } from "@notesnook/editor"; +import { useSpellChecker } from "../../../hooks/use-spell-checker"; +import { SpellCheckerLanguages } from "./components/spell-checker-languages"; +import { isDesktop } from "../../../utils/platform"; + +export const EditorSettings: SettingsGroup[] = [ + { + key: "editor", + section: "editor", + header: "Editor", + settings: [ + { + key: "default-title", + title: "Default title format", + description: `Use the following key to format the title: + +$date$: Current date +$time$: Current time +$count$: Number of notes + 1 +$headline$: Use starting line of the note as title +$timestamp$: Full date & time without any spaces or other +symbols (e.g. 202305261253)`, + onStateChange: (listener) => + useSettingStore.subscribe((c) => c.titleFormat, listener), + components: [ + { + type: "input", + inputType: "text", + defaultValue: () => useSettingStore.getState().titleFormat || "", + onChange: (value) => + useSettingStore.getState().setTitleFormat(value) + } + ] + }, + { + key: "default-font", + title: "Default font family", + onStateChange: (listener) => + onEditorConfigChange((c) => c.fontFamily, listener), + components: [ + { + type: "dropdown", + options: getFonts().map((font) => ({ + value: font.id, + title: font.title + })), + selectedOption: () => editorConfig().fontFamily, + onSelectionChanged: (value) => { + setEditorConfig({ fontFamily: value }); + } + } + ] + }, + { + key: "default-font-size", + title: "Default font size", + description: + "Change the default font size used in the editor. Minimum = 8px; Maximum = 120px.", + onStateChange: (listener) => + onEditorConfigChange((c) => c.fontSize, listener), + components: [ + { + type: "input", + inputType: "number", + max: 120, + min: 8, + defaultValue: () => editorConfig().fontSize, + onChange: (value) => setEditorConfig({ fontSize: value }) + } + ] + }, + { + key: "double-spacing", + title: "Double spaced paragraphs", + description: + "Use double spacing between paragraphs and when you press Enter in the editor.", + onStateChange: (listener) => + useSettingStore.subscribe((c) => c.doubleSpacedParagraphs, listener), + components: [ + { + type: "toggle", + isToggled: () => useSettingStore.getState().doubleSpacedParagraphs, + toggle: () => + useSettingStore.getState().toggleDoubleSpacedParagraphs() + } + ] + } + ] + }, + { + key: "spell-check", + section: "editor", + header: "Spell check", + isHidden: () => !isDesktop(), + onRender: () => { + useSpellChecker.getState().refresh(); + }, + settings: [ + { + key: "enable-spellchecker", + title: "Enable spell checker", + onStateChange: (listener) => + useSpellChecker.subscribe((c) => c.enabled, listener), + components: [ + { + type: "toggle", + isToggled: () => useSpellChecker.getState().enabled, + toggle: () => useSpellChecker.getState().toggleSpellChecker() + } + ] + }, + { + key: "spell-checker-languages", + title: "Languages", + description: "Select the languages the spell checker should check in.", + isHidden: () => !useSpellChecker.getState().enabled, + onStateChange: (listener) => + useSpellChecker.subscribe((c) => c.enabled, listener), + components: [ + { + type: "custom", + component: SpellCheckerLanguages + } + ] + } + ] + } +]; diff --git a/apps/web/src/stores/index.js b/apps/web/src/components/dialogs/settings/importer-settings.ts similarity index 76% rename from apps/web/src/stores/index.js rename to apps/web/src/components/dialogs/settings/importer-settings.ts index 48e1a93e9..ee8868674 100644 --- a/apps/web/src/stores/index.js +++ b/apps/web/src/components/dialogs/settings/importer-settings.ts @@ -17,15 +17,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -class BaseStore { - static new(set, get) { - return new this(set, get); - } +import { SettingsGroup } from "./types"; +import { Importer } from "./components/importer"; - constructor(set, get) { - this.set = set; - this.get = get; +export const ImporterSettings: SettingsGroup[] = [ + { + key: "importer", + section: "importer", + header: Importer, + settings: [] } -} - -export default BaseStore; +]; diff --git a/apps/web/src/components/dialogs/settings/index.tsx b/apps/web/src/components/dialogs/settings/index.tsx new file mode 100644 index 000000000..40beec60d --- /dev/null +++ b/apps/web/src/components/dialogs/settings/index.tsx @@ -0,0 +1,491 @@ +/* +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 . +*/ + +import { Flex, Text, Button, Input, Switch } from "@theme-ui/components"; +import Dialog from "../dialog"; +import { + About, + Account, + Appearance, + Backup, + Behaviour, + Desktop, + Documentation, + Editor, + Import, + Legal, + Loading, + Notification, + PasswordAndAuth, + Privacy, + Pro, + ShieldLock, + Sync +} from "../../icons"; +import { Perform } from "../../../common/dialog-controller"; +import NavigationItem from "../../navigation-menu/navigation-item"; +import { FlexScrollContainer } from "../../scroll-container"; +import { useCallback, useEffect, useState } from "react"; +import { SectionGroup, SectionKeys, Setting, SettingsGroup } from "./types"; +import { ProfileSettings } from "./profile-settings"; +import { AuthenticationSettings } from "./auth-settings"; +import { useIsUserPremium } from "../../../hooks/use-is-user-premium"; +import { store as userstore } from "../../../stores/user-store"; +import { SyncSettings } from "./sync-settings"; +import { BehaviourSettings } from "./behaviour-settings"; +import { DesktopIntegrationSettings } from "./desktop-integration-settings"; +import { NotificationsSettings } from "./notifications-settings"; +import { isDesktop } from "../../../utils/platform"; +import { BackupExportSettings } from "./backup-export-settings"; +import { ImporterSettings } from "./importer-settings"; +import { VaultSettings } from "./vault-settings"; +import { PrivacySettings } from "./privacy-settings"; +import { EditorSettings } from "./editor-settings"; +import { + AboutSettings, + LegalSettings, + SupportSettings +} from "./other-settings"; +import { AppearanceSettings } from "./appearance-settings"; +import { debounce } from "../../../utils/debounce"; +import { SubscriptionSettings } from "./subscription-settings"; + +type SettingsDialogProps = { onClose: Perform }; + +const sectionGroups: SectionGroup[] = [ + { + key: "account", + title: "User account", + sections: [ + { key: "profile", title: "Profile", icon: Account }, + { + key: "subscription", + title: "Subscription", + icon: Pro, + isHidden: () => !userstore.get().isLoggedIn + }, + { + key: "auth", + title: "Authentication", + icon: PasswordAndAuth, + isHidden: () => !userstore.get().isLoggedIn + }, + { + key: "sync", + title: "Sync", + icon: Sync, + isHidden: () => !userstore.get().isLoggedIn + } + ] + }, + { + key: "customization", + title: "Customization", + sections: [ + { key: "appearance", title: "Appearance", icon: Appearance }, + { key: "behaviour", title: "Behaviour", icon: Behaviour }, + { key: "editor", title: "Editor", icon: Editor }, + { + key: "desktop", + title: "Desktop integration", + icon: Desktop, + isHidden: () => !isDesktop() + }, + { key: "notifications", title: "Notifications", icon: Notification } + ] + }, + { + key: "import-export", + title: "Import & export", + sections: [ + { key: "backup-export", title: "Backup & export", icon: Backup }, + { key: "importer", title: "Notesnook Importer", icon: Import } + ] + }, + { + key: "security", + title: "Security & privacy", + sections: [ + { key: "vault", title: "Vault", icon: ShieldLock }, + { key: "privacy", title: "Privacy", icon: Privacy } + ] + }, + { + key: "other", + title: "Other", + sections: [ + { key: "legal", title: "Legal", icon: Legal }, + { key: "support", title: "Help and support", icon: Documentation }, + { key: "about", title: "About", icon: About } + ] + } +]; + +const SettingsGroups = [ + ...ProfileSettings, + ...AuthenticationSettings, + ...SyncSettings, + ...AppearanceSettings, + ...BehaviourSettings, + ...DesktopIntegrationSettings, + ...NotificationsSettings, + ...BackupExportSettings, + ...ImporterSettings, + ...VaultSettings, + ...PrivacySettings, + ...EditorSettings, + ...LegalSettings, + ...SupportSettings, + ...AboutSettings, + ...SubscriptionSettings +]; + +// Thoughts: +// 1. Settings will be conditional +// - For example settings which have an enablement placeholder +// - Or settings that appear after another setting is enabled. +// - Or settings that are visible after a user signs in +// 2. Settings will be synced so their state must be serializable +// 3. Settings will be grouped +// - Where group header is customizable +// 4. Sections/groups must be able to accomodate tips & tutorials for future. +// 5. Settings will be stateful but independent such that any one setting +// can appear independent of others (e.g. as a search result) + +export default function SettingsDialog(props: SettingsDialogProps) { + const [route, setRoute] = useState("profile"); + const [activeSettings, setActiveSettings] = useState( + SettingsGroups.filter((g) => g.section === route) + ); + + return ( + props.onClose(false)} + noScroll + sx={{ bg: "transparent" }} + > + + + + { + const query = e.target.value.toLowerCase().trim(); + if (!query) + return setActiveSettings( + SettingsGroups.filter((g) => g.section === route) + ); + + const groups: SettingsGroup[] = []; + for (const group of SettingsGroups) { + const isTitleMatch = + typeof group.header === "string" && + group.header.toLowerCase().includes(query); + + if (isTitleMatch) { + groups.push(group); + continue; + } + + const settings = group.settings.filter( + (setting) => + setting.description?.toLowerCase().includes(query) || + setting.keywords?.some((keyword) => + keyword.toLowerCase().includes(query) + ) || + setting.title.toLowerCase().includes(query) + ); + if (!settings.length) continue; + groups.push({ ...group, settings }); + } + setActiveSettings(groups); + }} + /> + {sectionGroups.map((group) => ( + + + {group.title} + + {group.sections.map( + (section) => + (!section.isHidden || !section.isHidden()) && ( + { + setActiveSettings( + SettingsGroups.filter( + (g) => g.section === section.key + ) + ); + setRoute(section.key); + }} + /> + ) + )} + + ))} + + + + + {activeSettings.map((group) => ( + + ))} + + + + + ); +} + +function SettingsGroupComponent(props: { item: SettingsGroup }) { + const { item } = props; + const { onRender } = item; + + useEffect(() => { + onRender?.(); + }, [onRender]); + + if (item.isHidden && item.isHidden()) return null; + return ( + + {typeof item.header === "string" ? ( + + {item.header.toUpperCase()} + + ) : ( + + )} + {item.settings.map((setting) => ( + + ))} + + ); +} + +function SettingItem(props: { item: Setting }) { + const { item } = props; + const [state, setState] = useState(); + const [isWorking, setIsWorking] = useState(false); + const isUserPremium = useIsUserPremium(); + + useEffect(() => { + if (!item.onStateChange) return; + item.onStateChange(setState); + }, [item]); + + const workWithLoading = useCallback( + async (action) => { + if (isWorking) return; + try { + setIsWorking(true); + await action(); + } finally { + setIsWorking(false); + } + }, + [isWorking] + ); + + if (item.isHidden && item.isHidden(state)) return null; + + const components = + typeof item.components === "function" + ? item.components(state) + : item.components; + + return ( + + + + {item.title} + {item.description && ( + + {item.description} + + )} + + + label": { width: "auto" } + }} + > + {components.map((component) => { + switch (component.type) { + case "button": + return ( + + ); + case "toggle": + return ( + workWithLoading(component.toggle)} + checked={component.isToggled()} + /> + ); + case "dropdown": + return ( + + ); + case "input": + return component.inputType === "number" ? ( + component.onChange(e.currentTarget.valueAsNumber), + 500 + )} + /> + ) : ( + component.onChange(e.currentTarget.value), + 500 + )} + /> + ); + } + })} + + + {components.map((component) => + component.type === "custom" ? ( + + ) : null + )} + + ); +} diff --git a/apps/web/src/components/dialogs/settings/notifications-settings.ts b/apps/web/src/components/dialogs/settings/notifications-settings.ts new file mode 100644 index 000000000..832d54b72 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/notifications-settings.ts @@ -0,0 +1,52 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; + +export const NotificationsSettings: SettingsGroup[] = [ + { + key: "reminders", + section: "notifications", + header: "Notifications", + settings: [ + { + key: "reminders", + title: "Show reminder notifications", + onStateChange: (listener) => + useSettingStore.subscribe( + (s) => s.notificationsSettings.reminder, + listener + ), + components: [ + { + type: "toggle", + isToggled: () => + !!useSettingStore.getState().notificationsSettings.reminder, + toggle: () => + useSettingStore.getState().setNotificationSettings({ + reminder: + !useSettingStore.getState().notificationsSettings.reminder + }) + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/other-settings.ts b/apps/web/src/components/dialogs/settings/other-settings.ts new file mode 100644 index 000000000..5517f8eb4 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/other-settings.ts @@ -0,0 +1,268 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { appVersion } from "../../../utils/version"; +import { writeText } from "clipboard-polyfill"; +import { showToast } from "../../../utils/toast"; +import { checkForUpdate } from "../../../utils/updater"; +import { isMacStoreApp } from "../../../utils/platform"; +import { showIssueDialog } from "../../../common/dialog-controller"; +import { clearLogs, downloadLogs } from "../../../utils/logger"; + +export const AboutSettings: SettingsGroup[] = [ + { + key: "about", + section: "about", + header: "About", + settings: [ + { + key: "version", + title: "Version", + description: appVersion.formatted, + components: [ + { + type: "button", + action: checkForUpdate, + title: "Check for updates", + variant: "secondary" + }, + { + type: "button", + action: async () => { + await writeText(appVersion.formatted); + showToast("info", "Copied to clipboard!"); + }, + title: "Copy", + variant: "secondary" + } + ] + }, + { + key: "roadmap", + title: "Roadmap", + description: + "See what we have planned for Notesnook in the coming months.", + components: [ + { + type: "button", + action: () => + void window.open("https://notesnook.com/roadmap", "_blank"), + title: "Check roadmap", + variant: "secondary" + } + ] + }, + { + key: "available-on-mobile", + title: isMacStoreApp() + ? "Available on iOS" + : "Available on iOS & Android", + description: isMacStoreApp() + ? "Get Notesnook app on your iPhone and access all your notes on the go." + : "Get Notesnook app on your iPhone or Android device and access all your notes on the go.", + components: [ + { + type: "button", + action: () => + void window.open( + isMacStoreApp() + ? "https://apps.apple.com/us/app/notesnook-take-private-notes/id1544027013" + : "https://notesnook.com/downloads", + "_blank" + ), + title: "Download", + variant: "secondary" + } + ] + } + ] + }, + { + key: "community", + section: "about", + header: "Community", + settings: [ + { + key: "telegram", + title: "Join our Telegram group", + description: "We are on Telegram. Let's have a chat!", + components: [ + { + type: "button", + action: () => void window.open("https://t.me/notesnook", "_blank"), + title: "Join Telegram", + variant: "secondary" + } + ] + }, + { + key: "mastodon", + title: "Follow us on Mastodon", + description: "We are on Mastodon!", + components: [ + { + type: "button", + action: () => void window.open("https://t.me/notesnook", "_blank"), + title: "Follow", + variant: "secondary" + } + ] + }, + { + key: "twitter", + title: "Follow us on Twitter", + description: + "We post regular updates, polls, and news about Notesnook and privacy. Follow us to stay updated!", + components: [ + { + type: "button", + action: () => + void window.open("https://twitter.com/notesnook", "_blank"), + title: "Follow", + variant: "secondary" + } + ] + }, + { + key: "discord", + title: "Join our Discord community", + description: + "We are not ghosts. Come chat with us and share your experience.", + components: [ + { + type: "button", + action: () => + void window.open( + "https://discord.com/invite/zQBK97EE22", + "_blank" + ), + title: "Join community", + variant: "secondary" + } + ] + } + ] + } +]; + +export const LegalSettings: SettingsGroup[] = [ + { + key: "legal", + section: "legal", + header: "Legal", + settings: [ + { + key: "privacy-policy", + title: "Privacy policy", + description: + "Your privacy is our first & foremost priority. Read our privacy policy to learn about how we protect your privacy while you take your notes.", + components: [ + { + type: "button", + action: () => + void window.open("https://notesnook.com/privacy", "_blank"), + title: "Read privacy policy", + variant: "secondary" + } + ] + }, + { + key: "tos", + title: "Terms of Service", + description: + "Read our terms of service to learn about what you have to agree to before using Notesnook.", + components: [ + { + type: "button", + action: () => + void window.open("https://notesnook.com/terms", "_blank"), + title: "Read terms", + variant: "secondary" + } + ] + } + ] + } +]; + +export const SupportSettings: SettingsGroup[] = [ + { + key: "support", + section: "support", + header: "Help and support", + settings: [ + { + key: "report-issue", + title: "Report an issue", + description: + "Facing an issue or have a suggestion? Send us a bug report so we can fix it ASAP!", + components: [ + { + type: "button", + action: showIssueDialog, + title: "Send bug report", + variant: "secondary" + } + ] + }, + { + key: "docs", + title: "Documentation", + description: "Learn about every feature in Notesnook and how it works.", + components: [ + { + type: "button", + action: () => + void window.open("https://help.notesnook.com/", "_blank"), + title: "Open docs", + variant: "secondary" + } + ] + } + ] + }, + { + key: "troubleshooting", + section: "support", + header: "Troubleshooting", + settings: [ + { + key: "download-logs", + title: "Debug logs", + description: + "All debug logs are stored locally & do not contain any sensitive information.", + components: [ + { + type: "button", + action: downloadLogs, + title: "Download", + variant: "secondary" + }, + { + type: "button", + action: clearLogs, + title: "Clear", + variant: "errorSecondary" + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/privacy-settings.ts b/apps/web/src/components/dialogs/settings/privacy-settings.ts new file mode 100644 index 000000000..ef3e4fbff --- /dev/null +++ b/apps/web/src/components/dialogs/settings/privacy-settings.ts @@ -0,0 +1,136 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useSettingStore } from "../../../stores/setting-store"; +import { useStore as useUserStore } from "../../../stores/user-store"; +import { getPlatform, isDesktop } from "../../../utils/platform"; +import { db } from "../../../common/db"; +import { showPromptDialog } from "../../../common/dialog-controller"; +import Config from "../../../utils/config"; +import { showToast } from "../../../utils/toast"; +import { TrackingDetails } from "./components/tracking-details"; + +export const PrivacySettings: SettingsGroup[] = [ + { + key: "general", + section: "privacy", + header: "General", + settings: [ + { + key: "telemetry", + title: "Telemetry", + description: `Usage data & crash reports will be sent to us (no 3rd party involved) for analytics. All data is anonymous as mentioned in our privacy policy. + +What data is collected & when?`, + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.telemetry, listener), + components: [ + { + type: "toggle", + isToggled: () => !!useSettingStore.getState().telemetry, + toggle: () => useSettingStore.getState().toggleTelemetry() + }, + { + type: "custom", + component: TrackingDetails + } + ] + }, + { + key: "marketing", + title: "Marketing emails", + description: + "We send you occasional promotional offers & product updates on your email (once every month).", + onStateChange: (listener) => + useUserStore.subscribe((s) => s.user?.marketingConsent, listener), + isHidden: () => !useUserStore.getState().isLoggedIn, + components: [ + { + type: "toggle", + isToggled: () => !!useUserStore.getState().user?.marketingConsent, + toggle: async () => { + await db.user?.changeMarketingConsent( + !useUserStore.getState().user?.marketingConsent + ); + await useUserStore.getState().refreshUser(); + } + } + ] + }, + { + key: "privacy-mode", + title: "Privacy mode", + description: + "Prevent Notesnook app from being captured by any screen capturing software like TeamViewer & AnyDesk.", + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.privacyMode, listener), + isHidden: () => !isDesktop() || getPlatform() === "linux", + components: [ + { + type: "toggle", + isToggled: () => useSettingStore.getState().privacyMode, + toggle: () => useSettingStore.getState().togglePrivacyMode() + } + ] + } + ] + }, + { + key: "advanced", + section: "privacy", + header: "Advanced", + settings: [ + { + 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", + onStateChange: (listener) => + useSettingStore.subscribe((s) => s.telemetry, listener), + components: [ + { + type: "button", + title: "Change proxy", + action: async () => { + const result = await showPromptDialog({ + title: "CORS bypass proxy", + description: + "You can set a custom proxy URL to increase your privacy.", + defaultValue: Config.get( + "corsProxy", + "https://cors.notesnook.com" + ) + }); + if (!result) return; + try { + const url = new URL(result); + Config.set("corsProxy", `${url.protocol}//${url.hostname}`); + } catch (e) { + console.error(e); + showToast("error", "Invalid CORS proxy url."); + } + }, + variant: "secondary" + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/profile-settings.ts b/apps/web/src/components/dialogs/settings/profile-settings.ts new file mode 100644 index 000000000..75154202b --- /dev/null +++ b/apps/web/src/components/dialogs/settings/profile-settings.ts @@ -0,0 +1,152 @@ +/* +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 . +*/ + +import { + useStore as useUserStore, + store as userstore +} from "../../../stores/user-store"; +import { SettingsGroup } from "./types"; +import { + showClearSessionsConfirmation, + showEmailChangeDialog, + showLoadingDialog, + showLogoutConfirmation, + showPasswordDialog, + showRecoveryKeyDialog +} from "../../../common/dialog-controller"; +import { db } from "../../../common/db"; +import { showToast } from "../../../utils/toast"; +import { UserProfile } from "./components/user-profile"; +import { verifyAccount } from "../../../common"; + +export const ProfileSettings: SettingsGroup[] = [ + { + key: "user-profile", + section: "profile", + header: UserProfile, + settings: [ + { + key: "email", + title: "Email", + description: "Set a new email for your account", + keywords: ["change email", "new email"], + isHidden: () => !useUserStore.getState().isLoggedIn, + components: [ + { + type: "button", + title: "Change email", + variant: "secondary", + action: showEmailChangeDialog + } + ] + }, + { + key: "recovery-key", + title: "Recovery key", + description: + "In case you lose your password, this data recovery key is the only way to recovery your data.", + keywords: ["data recovery key", "lose your password", "backup"], + isHidden: () => !userstore.get().isLoggedIn, + components: [ + { + type: "button", + title: "Backup your recovery key", + variant: "secondary", + action: async () => { + if (await verifyAccount()) await showRecoveryKeyDialog(); + } + } + ] + }, + { + key: "account-removal", + title: "Account removal", + description: + "Permanently delete your account clearing all data including your notes, notebooks, and attachments.", + keywords: ["delete account", "clear data"], + isHidden: () => !userstore.get().isLoggedIn, + components: [ + { + type: "button", + variant: "error", + title: "Delete account", + action: async () => + showPasswordDialog("delete_account", async (password) => { + await db.user?.deleteUser(password); + return true; + }) + } + ] + } + ] + }, + { + key: "user-sessions", + section: "profile", + header: "Sessions", + isHidden: () => !userstore.get().isLoggedIn, + settings: [ + { + key: "logout", + title: "Logout", + description: "Logging out will clear all data on this device.", + keywords: [], + components: [ + { + type: "button", + variant: "errorSecondary", + title: "Logout", + action: async () => { + if (await showLogoutConfirmation()) { + await showLoadingDialog({ + title: "You are being logged out", + subtitle: "Please wait...", + action: () => db.user?.logout(true) + }); + showToast("success", "You have been logged out."); + } + } + } + ] + }, + { + key: "logout-all-sessions", + title: "Log out from all other devices", + description: "Force logout from all your other logged in devices.", + keywords: ["clear sessions"], + components: [ + { + type: "button", + variant: "errorSecondary", + title: "Log out all other devices", + action: async () => { + if (!(await showClearSessionsConfirmation())) return; + + await db.user?.clearSessions(); + showToast( + "success", + "You have been logged out from all other devices." + ); + } + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/subscription-settings.ts b/apps/web/src/components/dialogs/settings/subscription-settings.ts new file mode 100644 index 000000000..57e31024e --- /dev/null +++ b/apps/web/src/components/dialogs/settings/subscription-settings.ts @@ -0,0 +1,69 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { SubscriptionStatus } from "./components/subscription-status"; +import { showToast } from "../../../utils/toast"; +import { db } from "../../../common/db"; +import { BillingHistory } from "./components/billing-history"; +import { useStore as useUserStore } from "../../../stores/user-store"; +import { isUserSubscribed } from "../../../hooks/use-is-user-premium"; + +export const SubscriptionSettings: SettingsGroup[] = [ + { + key: "subscription", + section: "subscription", + header: SubscriptionStatus, + settings: [ + { + key: "payment-method", + title: "Payment method", + description: + "Change the payment method you used to purchase this subscription.", + isHidden: () => { + const user = useUserStore.getState().user; + return !isUserSubscribed(user) || user?.subscription.provider !== 3; + }, + components: [ + { + type: "button", + title: "Update", + action: async () => { + try { + window.open(await db.subscriptions?.updateUrl(), "_blank"); + } catch (e) { + if (e instanceof Error) showToast("error", e.message); + } + }, + variant: "secondary" + } + ] + }, + { + key: "billing-history", + title: "Billing history", + isHidden: () => { + const user = useUserStore.getState().user; + return !isUserSubscribed(user) || user?.subscription.provider !== 3; + }, + components: [{ type: "custom", component: BillingHistory }] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/sync-settings.ts b/apps/web/src/components/dialogs/settings/sync-settings.ts new file mode 100644 index 000000000..f8dd19d46 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/sync-settings.ts @@ -0,0 +1,94 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useAppStore } from "../../../stores/app-store"; + +export const SyncSettings: SettingsGroup[] = [ + { + key: "sync", + section: "sync", + header: "Sync", + settings: [ + { + key: "toggle-sync", + title: "Enable sync", + description: + "Disable syncing to prevent all changes from syncing to & from other devices.", + keywords: ["sync off", "toggle sync"], + onStateChange: (listener) => + useAppStore.subscribe((s) => s.isSyncEnabled, listener), + components: [ + { + type: "toggle", + isToggled: () => useAppStore.getState().isSyncEnabled, + toggle: () => useAppStore.getState().toggleSync() + } + ] + }, + { + key: "toggle-auto-sync", + title: "Enable auto sync", + description: + "Disable auto sync to prevent changes from automatically syncing to other devices. This will require manually pressing the sync button in order to sync changes.", + keywords: ["auto sync off", "automatic sync", "toggle auto sync"], + onStateChange: (listener) => + useAppStore.subscribe((s) => s.isSyncEnabled, listener), + components: [ + { + type: "toggle", + isToggled: () => useAppStore.getState().isAutoSyncEnabled, + toggle: () => useAppStore.getState().toggleAutoSync() + } + ] + }, + + { + key: "toggle-realtime-sync", + title: "Enable realtime editor sync", + description: + "Disable realtime editor sync to prevent edits from updating in realtime on this device. This will require closing and opening the note to see new changes.", + keywords: ["auto sync off", "automatic sync", "toggle auto sync"], + onStateChange: (listener) => + useAppStore.subscribe((s) => s.isSyncEnabled, listener), + components: [ + { + type: "toggle", + isToggled: () => useAppStore.getState().isRealtimeSyncEnabled, + toggle: () => useAppStore.getState().toggleRealtimeSync() + } + ] + }, + { + key: "force-sync", + title: "Having problems with sync?", + description: "Try force sync to resolve issues with syncing.", + keywords: ["force sync", "sync troubleshoot"], + components: [ + { + type: "button", + title: "Force sync", + variant: "error", + action: () => useAppStore.getState().sync(true, true) + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/settings/types.ts b/apps/web/src/components/dialogs/settings/types.ts new file mode 100644 index 000000000..fbd0f7a81 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/types.ts @@ -0,0 +1,137 @@ +/* +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 . +*/ + +import { Icon } from "../../icons"; + +export type SectionKeys = + | "profile" + | "auth" + | "subscription" + | "sync" + | "appearance" + | "behaviour" + | "desktop" + | "notifications" + | "editor" + | "backup-export" + | "export" + | "importer" + | "vault" + | "privacy" + | "support" + | "legal" + | "developer" + | "about"; + +export type SectionGroupKeys = + | "account" + | "customization" + | "import-export" + | "security" + | "other"; + +export type SectionGroup = { + key: SectionGroupKeys; + title: string; + sections: Section[]; +}; + +export type Section = { + key: SectionKeys; + title: string; + icon: Icon; + isHidden?: () => boolean; +}; + +export type SettingsGroup = { + key: string; + section: SectionKeys; + settings: Setting[]; + header: string | ((props: any) => JSX.Element | null); + isHidden?: () => boolean; + onRender?: () => void | Promise; +}; + +export type Setting = { + key: string; + keywords?: string[]; + title: string; + description?: string; + components: SettingComponent[] | ((state?: unknown) => SettingComponent[]); + isHidden?: (state?: unknown) => boolean; + onStateChange?: ( + listener: (state: unknown, prevState: unknown) => void + ) => void; +}; + +export type SettingComponentType = + | "toggle" + | "dropdown" + | "button" + | "input" + | "custom"; + +export type SettingComponent = + | ButtonSettingComponent + | ToggleSettingComponent + | DropdownSettingComponent + | InputSettingComponent + | CustomSettingComponent; + +export type BaseSettingComponent = { + type: TType; +}; + +export type ButtonSettingComponent = BaseSettingComponent<"button"> & { + title: string; + action: () => void | Promise; + variant: "primary" | "secondary" | "error" | "errorSecondary"; +}; + +export type ToggleSettingComponent = BaseSettingComponent<"toggle"> & { + isToggled: () => boolean; + toggle: () => void | Promise; +}; + +export type DropdownSettingComponent = BaseSettingComponent<"dropdown"> & { + options: { value: string; title: string; premium?: boolean }[]; + selectedOption: () => string; + onSelectionChanged: (value: string) => void | Promise; +}; + +export type InputSettingComponent = BaseSettingComponent<"input"> & + (TextInputSettingComponent | NumberInputSettingComponent); + +export type NumberInputSettingComponent = BaseSettingComponent<"input"> & { + inputType: "number"; + min: number; + max: number; + defaultValue: () => number; + onChange: (value: number) => void; +}; + +export type TextInputSettingComponent = BaseSettingComponent<"input"> & { + inputType: "text"; + defaultValue: () => string; + onChange: (value: string) => void; +}; + +export type CustomSettingComponent = BaseSettingComponent<"custom"> & { + component: () => JSX.Element; +}; diff --git a/apps/web/src/components/dialogs/settings/vault-settings.tsx b/apps/web/src/components/dialogs/settings/vault-settings.tsx new file mode 100644 index 000000000..484650906 --- /dev/null +++ b/apps/web/src/components/dialogs/settings/vault-settings.tsx @@ -0,0 +1,102 @@ +/* +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 . +*/ + +import { SettingsGroup } from "./types"; +import { useStore as useAppStore } from "../../../stores/app-store"; +import { useStore as useNotesStore } from "../../../stores/note-store"; +import { hashNavigate } from "../../../navigation"; +import Vault from "../../../common/vault"; +import { showToast } from "../../../utils/toast"; +import { db } from "../../../common/db"; + +export const VaultSettings: SettingsGroup[] = [ + { + key: "vault", + section: "vault", + header: "Vault", + settings: [ + { + key: "reminders", + title: "Create vault", + isHidden: () => useAppStore.getState().isVaultCreated, + components: [ + { + type: "button", + title: "Create", + action: () => hashNavigate("/vault/create"), + variant: "secondary" + } + ] + }, + { + key: "reminders", + title: "Change vault password", + description: "Set a new password for your vault", + isHidden: () => !useAppStore.getState().isVaultCreated, + components: [ + { + type: "button", + title: "Change", + action: () => hashNavigate("/vault/create"), + variant: "secondary" + } + ] + }, + { + key: "reminders", + title: "Clear vault", + description: "Unlock all locked notes and clear vault.", + isHidden: () => !useAppStore.getState().isVaultCreated, + components: [ + { + type: "button", + title: "Clear", + action: async () => { + if (await Vault.clearVault()) { + useNotesStore.getState().refresh(); + showToast("success", "Vault cleared."); + } + }, + variant: "errorSecondary" + } + ] + }, + { + key: "reminders", + title: "Delete vault", + description: "Delete vault including all locked notes.", + isHidden: () => !useAppStore.getState().isVaultCreated, + components: [ + { + type: "button", + title: "Delete", + action: async () => { + if ((await Vault.deleteVault()) && !(await db.vault?.exists())) { + useAppStore.getState().setIsVaultCreated(false); + await useAppStore.getState().refresh(); + showToast("success", "Vault deleted."); + } + }, + variant: "errorSecondary" + } + ] + } + ] + } +]; diff --git a/apps/web/src/components/dialogs/tracking-details-dialog.jsx b/apps/web/src/components/dialogs/tracking-details-dialog.jsx deleted file mode 100644 index 77a9c2063..000000000 --- a/apps/web/src/components/dialogs/tracking-details-dialog.jsx +++ /dev/null @@ -1,70 +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 . -*/ - -import { Box, Text } from "@theme-ui/components"; -import { ANALYTICS_EVENTS } from "../../utils/analytics"; -import Dialog from "./dialog"; - -const events = Object.values(ANALYTICS_EVENTS); -function TrackingDetailsDialog(props) { - return ( - props.onClose(false)} - positiveButton={{ - text: "Okay", - onClick: () => props.onClose(true) - }} - > - - - - - Event name - {" "} - - Event detail - - - {events.map((event) => ( - - - {event.name} - - - {event.description} - - - ))} -
-
-
- ); -} - -export default TrackingDetailsDialog; diff --git a/apps/web/src/components/editor/context.ts b/apps/web/src/components/editor/context.ts index 9a06b0aca..6244f39c7 100644 --- a/apps/web/src/components/editor/context.ts +++ b/apps/web/src/components/editor/context.ts @@ -21,8 +21,7 @@ import { useCallback, useEffect, useRef } from "react"; import { IEditor, NoteStatistics } from "./types"; import createStore from "../../common/store"; import BaseStore from "../../stores"; -import { UseBoundStore } from "zustand"; -import shallow from "zustand/shallow"; +import { shallow } from "zustand/shallow"; import type { ToolbarDefinition } from "@notesnook/editor"; import Config from "../../utils/config"; @@ -37,7 +36,7 @@ type EditorSubState = { statistics?: NoteStatistics; }; -class EditorContext extends BaseStore { +class EditorContext extends BaseStore { subState: EditorSubState = { editorConfig: Config.get("editorConfig", { fontFamily: "sans-serif", @@ -50,7 +49,7 @@ class EditorContext extends BaseStore { | Partial | ((oldState: EditorSubState) => Partial) ) => { - this.set((state: EditorContext) => { + this.set((state) => { const newPartialState = typeof partial === "function" ? partial(state.subState) : partial; state.subState = { ...state.subState, ...newPartialState }; @@ -58,10 +57,7 @@ class EditorContext extends BaseStore { }; } -const [useEditorContext] = createStore(EditorContext) as [ - UseBoundStore, - EditorContext -]; +const [useEditorContext] = createStore(EditorContext); export function useEditorInstance() { const editor = useEditorContext((store) => store.subState.editor); @@ -128,24 +124,35 @@ export function useNoteStatistics(): NoteStatistics { export function useEditorConfig() { const editorConfig = useEditorContext((store) => store.subState.editorConfig); - const configure = useEditorContext((store) => store.configure); - const setEditorConfig = useCallback( - (config: Partial) => { - if (editorConfig) - Config.set("editorConfig", { - ...editorConfig, - ...config - }); - - configure({ - editorConfig: { - ...editorConfig, - ...config - } - }); - }, - [editorConfig, configure] - ); - return { editorConfig, setEditorConfig }; } + +export const editorConfig = () => + useEditorContext.getState().subState.editorConfig; + +export const setEditorConfig = (config: Partial) => { + const oldConfig = editorConfig(); + if (oldConfig) + Config.set("editorConfig", { + ...oldConfig, + ...config + }); + + useEditorContext.getState().configure({ + editorConfig: { + ...oldConfig, + ...config + } + }); +}; +export const onEditorConfigChange = ( + selector: (editorConfig: EditorConfig) => any, + listener: ( + selectedState: EditorConfig, + previousSelectedState: EditorConfig + ) => void +) => + useEditorContext.subscribe( + (s) => selector(s.subState.editorConfig), + listener + ); diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index 2eb533173..3a51f51d7 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -70,7 +70,11 @@ type DocumentPreview = { hash: string; }; -function onEditorChange(noteId: string, sessionId: string, content: string) { +function onEditorChange( + noteId: string | undefined, + sessionId: number, + content: string +) { if (!content) return; editorstore.get().saveSessionContent(noteId, sessionId, { @@ -338,7 +342,7 @@ type EditorOptions = { onLoadMedia?: () => void; }; type EditorProps = { - content: () => string; + content: () => string | undefined; nonce?: number; options?: EditorOptions; onContentChange?: () => void; diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index e770af131..d91592dc5 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -58,17 +58,22 @@ import { useStore as useSettingsStore } from "../../stores/setting-store"; import { debounce, debounceWithId } from "../../utils/debounce"; import { store as editorstore } from "../../stores/editor-store"; +type OnChangeHandler = ( + id: string | undefined, + sessionId: number, + content: string +) => void; type TipTapProps = { editorContainer: HTMLElement; onLoad?: () => void; - onChange?: (id: string, sessionId: string, content: string) => void; + onChange?: OnChangeHandler; onContentChange?: () => void; onInsertAttachment?: (type: AttachmentType) => void; onDownloadAttachment?: (attachment: Attachment) => void; onPreviewAttachment?: (attachment: Attachment) => void; onAttachFile?: (file: File) => void; onFocus?: () => void; - content?: () => string; + content?: () => string | undefined; toolbarContainerId?: string; readonly?: boolean; nonce?: number; @@ -82,12 +87,12 @@ type TipTapProps = { const SAVE_INTERVAL = import.meta.env.REACT_APP_TEST ? 100 : 300; function save( - sessionId: string, - noteId: string, + sessionId: number, + noteId: string | undefined, editor: Editor, content: Fragment, preventSave: boolean, - onChange?: (id: string, sessionId: string, html: string) => void + onChange?: OnChangeHandler ) { configureEditor({ statistics: { @@ -130,7 +135,7 @@ function TipTap(props: TipTapProps) { const isUserPremium = useIsUserPremium(); const configure = useConfigureEditor(); const doubleSpacedLines = useSettingsStore( - (store) => store.doubleSpacedLines + (store) => store.doubleSpacedParagraphs ); const dateFormat = useSettingsStore((store) => store.dateFormat); const timeFormat = useSettingsStore((store) => store.timeFormat); @@ -205,7 +210,6 @@ function TipTap(props: TipTapProps) { const preventSave = transaction?.getMeta("preventSave") as boolean; const { id, sessionId } = editorstore.get().session; const content = editor.state.doc.content; - deferredSave( sessionId, sessionId, diff --git a/apps/web/src/components/editor/title-box.tsx b/apps/web/src/components/editor/title-box.tsx index 2647c459d..fa49dbd7d 100644 --- a/apps/web/src/components/editor/title-box.tsx +++ b/apps/web/src/components/editor/title-box.tsx @@ -94,7 +94,7 @@ export default React.memo(TitleBox, (prevProps, nextProps) => { return prevProps.readonly === nextProps.readonly; }); -function onTitleChange(noteId: string, title: string) { +function onTitleChange(noteId: string | undefined, title: string) { store.get().setTitle(noteId, title); } diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/icons/index.tsx index 28c4a1586..ff5e09d97 100644 --- a/apps/web/src/components/icons/index.tsx +++ b/apps/web/src/components/icons/index.tsx @@ -201,7 +201,17 @@ import { mdiMagnifyPlusOutline, mdiMagnifyMinusOutline, mdiRotateRight, - mdiRotateLeft + mdiRotateLeft, + mdiKeyOutline, + mdiDatabaseImportOutline, + mdiDeveloperBoard, + mdiInformationOutline, + mdiHeadCogOutline, + mdiFormTextarea, + mdiDatabaseExportOutline, + mdiGavel, + mdiDesktopClassic, + mdiBellBadgeOutline } from "@mdi/js"; import { useTheme } from "@emotion/react"; import { Theme } from "@notesnook/theme"; @@ -505,3 +515,17 @@ export const ZoomOut = createIcon(mdiMagnifyMinusOutline); export const RotateCW = createIcon(mdiRotateRight); export const RotateACW = createIcon(mdiRotateLeft); export const Reset = createIcon(mdiRestore); + +export const Account = createIcon(mdiAccountOutline); +export const PasswordAndAuth = createIcon(mdiKeyOutline); +export const Appearance = createIcon(mdiPaletteSwatchOutline); +export const Import = createIcon(mdiDatabaseImportOutline); +export const Privacy = createIcon(mdiEyeOffOutline); +export const Developer = createIcon(mdiDeveloperBoard); +export const About = createIcon(mdiInformationOutline); +export const Behaviour = createIcon(mdiHeadCogOutline); +export const Editor = createIcon(mdiFormTextarea); +export const Documentation = createIcon(mdiFileDocumentOutline); +export const Legal = createIcon(mdiGavel); +export const Desktop = createIcon(mdiDesktopClassic); +export const Notification = createIcon(mdiBellBadgeOutline); diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index db490e411..18ef8c598 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -39,7 +39,7 @@ import { } from "../icons"; import { AnimatedFlex } from "../animated"; import NavigationItem from "./navigation-item"; -import { hardNavigate, navigate } from "../../navigation"; +import { hardNavigate, hashNavigate, navigate } from "../../navigation"; import { db } from "../../common/db"; import useMobile from "../../hooks/use-mobile"; import { showRenameColorDialog } from "../../common/dialog-controller"; @@ -282,11 +282,8 @@ function NavigationMenu(props: NavigationMenuProps) { title={settings.title} icon={settings.icon} onClick={() => { - if (!isMobile && location === settings.path) - return toggleNavigationContainer(); - _navigate(settings.path); + hashNavigate("/settings"); }} - selected={location.startsWith(settings.path)} > {isTablet ? null : ( - - - - - - - )} - - {isLoggedIn && user.mfa && ( - <> -
{ - setGroups((g) => ({ ...g, mfa: !g.mfa })); - }} - /> - {groups.mfa && ( - <> - - - - - - )} - - )} - {isLoggedIn && ( - <> -
{ - setGroups((g) => ({ ...g, sync: !g.sync })); - }} - /> - {groups.sync && ( - <> - - - - - - )} - - )} -
{ - setGroups((g) => ({ ...g, appearance: !g.appearance })); - }} - /> - {groups.appearance && ( - <> - - - {getAllAccents().map((color) => ( - - ))} - - - - - {isDesktop() && ( - <> - - { - setZoomFactor(e.target.valueAsNumber); - }, 500)} - /> - - )} - - )} -
{ - setGroups((g) => ({ ...g, behaviour: !g.behaviour })); - }} - /> - {groups.behaviour && ( - <> - setHomepage(index)} - /> - - { - setTrashCleanupInterval(option.value); - await db.settings.setTrashCleanupInterval(option.value); - }} - /> - - settingstore.get().setTimeFormat(option.value) - } - /> - - - - settingstore.get().setDateFormat(e.target.value) - } - > - {DATE_FORMATS.map((format) => ( - - ))} - - - - - - settingstore.get().setTitleFormat(e.target.value) - } - /> - - Use the following key to format the title: -
-
- $date$: Current date -
- $time$: Current time -
- $count$: Number of notes + 1
- $headline$: Use starting line of the note as title -
- $timestamp$: Full date & time without any spaces or other - symbols (e.g. 202305261253) -
-
- - - )} - {isDesktop() && ( - <> -
{ - setGroups((g) => ({ ...g, desktop: !g.desktop })); - }} - /> - {groups.desktop && ( - <> - { - changeDesktopIntegration({ - autoStart: !desktopIntegration.autoStart - }); - }} - isToggled={desktopIntegration.autoStart} - /> - {desktopIntegration.autoStart && ( - { - changeDesktopIntegration({ - startMinimized: !desktopIntegration.startMinimized - }); - }} - isToggled={desktopIntegration.startMinimized} - /> - )} - { - changeDesktopIntegration({ - minimizeToSystemTray: - !desktopIntegration.minimizeToSystemTray - }); - }} - isToggled={desktopIntegration.minimizeToSystemTray} - /> - { - changeDesktopIntegration({ - closeToSystemTray: !desktopIntegration.closeToSystemTray - }); - }} - isToggled={desktopIntegration.closeToSystemTray} - /> - - )} - - )} - -
{ - setGroups((g) => ({ ...g, editor: !g.editor })); - }} - /> - {groups.editor && ( - <> - ({ - value: font.id, - title: font.title - }))} - onSelectionChanged={(option) => { - setEditorConfig({ fontFamily: option.value }); - }} - /> - - - { - setEditorConfig({ fontSize: e.currentTarget.valueAsNumber }); - }} - /> - - {/* {}} - onIncrease={() => {}} - title="Set default font size" - onReset={() => setEditorConfig({ fontFamily: "16px" })} - /> */} - {/* */} - { - toggleDoubleSpacedLines(); - showToast( - "success", - "Re-open the editor for changes to take effect." - ); - }} - isToggled={doubleSpacedLines} - /> - - - {isDesktop() && ( - <> - spellChecker.toggle(!spellChecker.enabled)} - isToggled={spellChecker.enabled} - /> - {getPlatform() !== "darwin" && ( - - )} - - )} - - )} - -
{ - setGroups((g) => ({ ...g, notifications: !g.notifications })); - }} - /> - {groups.notifications && ( - <> - setShowReminderNotifications((s) => !s)} - isToggled={showReminderNotifications} - /> - - )} -
{ - setGroups((g) => ({ ...g, backup: !g.backup })); - }} - /> - - {groups.backup && ( - <> - - { - await db.notes.init(); - await exportNotes( - option.value, - db.notes.all.map((n) => n.id) - ); - }} - /> - {(isLoggedIn || isTesting()) && ( - <> - - - - - setBackupReminderOffset(option.value) - } - /> - {isDesktop() ? ( - - ) : null} - - )} - - )} - -
{ - setGroups((g) => ({ ...g, importer: !g.importer })); - }} - /> - {groups.importer && ( - <> - - - )} - -
{ - setGroups((g) => ({ ...g, privacy: !g.privacy })); - }} - /> - {groups.privacy && ( - <> - {isVaultCreated ? ( - <> - - - - - ) : ( - - )} - - { - await db.user.changeMarketingConsent(!user.marketingConsent); - await refreshUser(); - }} - isToggled={user.marketingConsent} - /> - { - setEnableTelemetry(!enableTelemetry); - }} - isToggled={enableTelemetry} - /> - - {isDesktop() && getPlatform() !== "linux" && ( - { - setPrivacyMode(!privacyMode); - }} - isToggled={privacyMode} - /> - )} - - )} - -
{ - setGroups((g) => ({ ...g, developer: !g.developer })); - }} - /> - {groups.developer && ( - <> - setDebugMode(!debugMode)} - isToggled={debugMode} - /> - - - {isDesktop() && ( - - )} - - )} - -
{ - setGroups((g) => ({ ...g, other: !g.other })); - }} - /> - - {groups.other && ( - <> - {otherItems.map((item) => ( - - ))} - - )} - {isLoggedIn && ( - - - DANGER ZONE - - - - - )} - - - ); -} - -export default Settings; - -function OptionsItem(props) { - const { - title, - tip, - options, - selectedOption, - onSelectionChanged, - onlyIf, - premium - } = props; - - if (onlyIf === false) return null; - return ( - - - - {options.map((option, index) => ( - { - const isPremium = premium || option.premium; - if (isUserPremium() || !isPremium) - onSelectionChanged(option, index); - else { - await showBuyDialog(); - } - }} - sx={{ - ":hover": { - color: selectedOption === option.value ? "static" : "text" - }, - flex: 1, - textAlign: "center", - color: - selectedOption === option.value ? "static" : "bgSecondaryText", - minWidth: 70 - }} - > - {option.title} - - ))} - - - ); -} - -function AccountStatus(props) { - const { user } = props; - const { - isTrial, - isBeta, - isPro, - isBasic, - isProCancelled, - isProExpired, - remainingDays - } = useMemo(() => { - const type = user?.subscription?.type; - const expiry = user?.subscription?.expiry; - if (!type || !expiry) return { isBasic: true }; - return { - remainingDays: dayjs(expiry).diff(dayjs(), "day"), - isTrial: type === SUBSCRIPTION_STATUS.TRIAL, - isBasic: type === SUBSCRIPTION_STATUS.BASIC, - isBeta: type === SUBSCRIPTION_STATUS.BETA, - isPro: type === SUBSCRIPTION_STATUS.PREMIUM, - isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED, - isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED - }; - }, [user]); - - const subtitle = useMemo(() => { - const expiryDate = dayjs(user?.subscription?.expiry).format("MMMM D, YYYY"); - const startDate = dayjs(user?.subscription?.start).format("MMMM D, YYYY"); - return isPro - ? `Your subscription will auto renew on ${expiryDate}.` - : isProCancelled - ? `Your subscription will end on ${expiryDate}.` - : isProExpired - ? "Your account will be downgraded to Basic in 3 days." - : isBeta - ? `Your were enrolled in our beta program on ${startDate}` - : isTrial - ? `Your trial will end on ${expiryDate}` - : null; - }, [isPro, isProExpired, isProCancelled, isBeta, isTrial, user]); - - const provider = useMemo(() => { - const provider = user?.subscription?.provider; - switch (provider) { - default: - case 0: - return "Streetwriters"; - case 1: - return "iOS"; - case 2: - return "Android"; - case 3: - return "Web"; - } - }, [user]); - - return ( - - - {remainingDays > 0 && isPro - ? `Subscribed to Notesnook Pro` - : remainingDays > 0 && isTrial - ? "You are on free trial" - : isBeta - ? "Your beta subscription has ended" - : isTrial - ? "Your trial has ended" - : isPro - ? "Your Notesnook Pro subscription has ended" - : ""} - - {subtitle && {subtitle}} - {isBasic || - isTrial || - isProExpired || - isProCancelled || - remainingDays <= 0 ? ( - - ) : provider === "Streetwriters" ? ( - <> - - Awarded by {provider} - - - ) : isPro ? ( - <> - {provider === "Web" ? ( - <> - - - - - - ) : null} - - Purchased on {provider} - - - ) : null} - - ); -} - -function AccountStatusContainer(props) { - const { bg, color, user, children } = props; - return ( - - - - - - {user.email} - - - - {subscriptionStatusToString(user)} - - - {children} - - ); -} - -function Header(props) { - const { title, isOpen, testId, onClick } = props; - return ( - - - {title} - - {isOpen ? ( - - ) : ( - - )} - - ); -} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 9b80428ef..0f3354c43 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -139,7 +139,9 @@ const WEB_MANIFEST: Partial = { categories: ["productivity", "lifestyle", "education", "books"] }; -const isTesting = process.env.REACT_APP_TEST === "true"; +const isTesting = + process.env.REACT_APP_TEST === "true" || + process.env.NODE_ENV === "development"; export default defineConfig({ envPrefix: "REACT_APP_", build: { diff --git a/packages/core/types.js b/packages/core/types.js index 2ee41cd9a..1eeaf3863 100644 --- a/packages/core/types.js +++ b/packages/core/types.js @@ -39,8 +39,8 @@ const _ignore = ""; * marketingConsent: boolean, * mfa: { * isEnabled: boolean, - * primaryMethod: string, - * secondaryMethod: string, + * primaryMethod: "app" | "sms" | "email", + * secondaryMethod: "app" | "sms" | "email", * remainingValidCodes: number * }, * subscription: { diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index 3af1db89e..6c1001a8a 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -47,7 +47,7 @@ "strip-indent": "^4.0.0", "tinycolor2": "^1.4.2", "unfurl.js": "^5.7.0", - "zustand": "^3.7.2" + "zustand": "^4.3.8" }, "devDependencies": { "@mdi/js": "^6.9.96", @@ -3550,6 +3550,14 @@ "node": ">=6.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3856,16 +3864,23 @@ } }, "node_modules/zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, "engines": { "node": ">=12.7.0" }, "peerDependencies": { + "immer": ">=9.0", "react": ">=16.8" }, "peerDependenciesMeta": { + "immer": { + "optional": true + }, "react": { "optional": true } @@ -6367,6 +6382,11 @@ "source-map-support": "^0.5.9" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6558,9 +6578,12 @@ "dev": true }, "zustand": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", - "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==" + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "requires": { + "use-sync-external-store": "1.2.0" + } } } } diff --git a/packages/editor/package.json b/packages/editor/package.json index 5e2512dd3..7e06d9f33 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -45,7 +45,7 @@ "strip-indent": "^4.0.0", "tinycolor2": "^1.4.2", "unfurl.js": "^5.7.0", - "zustand": "^3.7.2" + "zustand": "^4.3.8" }, "devDependencies": { "@mdi/js": "^6.9.96", diff --git a/packages/editor/src/toolbar/stores/toolbar-store.ts b/packages/editor/src/toolbar/stores/toolbar-store.ts index b05bbe1cf..4af6d7a26 100644 --- a/packages/editor/src/toolbar/stores/toolbar-store.ts +++ b/packages/editor/src/toolbar/stores/toolbar-store.ts @@ -18,7 +18,7 @@ along with this program. If not, see . */ import { Theme } from "@notesnook/theme"; -import create from "zustand"; +import { create } from "zustand"; import { DownloadOptions } from "../../utils/downloader"; export type ToolbarLocation = "top" | "bottom"; @@ -50,37 +50,25 @@ export const useToolbarStore = create((set, get) => ({ downloadOptions: undefined, isMobile: false, openedPopups: {}, - setDownloadOptions: (options) => - set((state) => { - state.downloadOptions = options; - }), - setIsMobile: (isMobile) => - set((state) => { - state.isMobile = isMobile; - }), - setTheme: (theme) => - set((state) => { - state.theme = theme; - }), + setDownloadOptions: (options) => set({ downloadOptions: options }), + setIsMobile: (isMobile) => set({ isMobile }), + setTheme: (theme) => set({ theme }), toolbarLocation: "top", - setToolbarLocation: (location) => - set((state) => { - state.toolbarLocation = location; - }), + setToolbarLocation: (location) => set({ toolbarLocation: location }), closePopup: (id) => - set((state) => { - state.openedPopups = { - ...state.openedPopups, + set({ + openedPopups: { + ...get().openedPopups, [id]: undefined - }; + } }), isPopupOpen: (id) => !!get().openedPopups[id], openPopup: (ref) => - set((state) => { - state.openedPopups = { - ...state.openedPopups, + set({ + openedPopups: { + ...get().openedPopups, [ref.id]: ref - }; + } }), closePopupGroup: (group, excluded) => set((state) => { @@ -90,23 +78,13 @@ export const useToolbarStore = create((set, get) => ({ state.openedPopups[key] = undefined; } } + return state; }), - closeAllPopups: () => - set((state) => { - for (const key in state.openedPopups) { - state.openedPopups[key] = undefined; - } - }), + closeAllPopups: () => set({ openedPopups: {} }), fontFamily: "sans-serif", - setFontFamily: (fontFamily) => - set((state) => { - state.fontFamily = fontFamily; - }), + setFontFamily: (fontFamily) => set({ fontFamily }), fontSize: 16, - setFontSize: (fontSize) => - set((state) => { - state.fontSize = fontSize; - }) + setFontSize: (fontSize) => set({ fontSize }) })); export function useToolbarLocation() { diff --git a/packages/theme/src/theme/variants/button.ts b/packages/theme/src/theme/variants/button.ts index d3ee84906..5235ba6ed 100644 --- a/packages/theme/src/theme/variants/button.ts +++ b/packages/theme/src/theme/variants/button.ts @@ -26,7 +26,9 @@ const defaultVariant: ThemeUIStyleObject = { fontSize: "body", borderRadius: "default", cursor: "pointer", - p: 2, + // p: 0, + height: "min-content", + px: 2, py: "7.5px", transition: "filter 200ms ease-in, box-shadow 200ms ease-out", ":hover:not(:disabled)": { @@ -52,6 +54,28 @@ const primary: ThemeUIStyleObject = { bg: "primary" }; +const error: ThemeUIStyleObject = { + variant: "buttons.default", + color: "static", + bg: "error" +}; + +const errorSecondary: ThemeUIStyleObject = { + variant: "buttons.default", + color: "error", + // fontWeight: "bold", + bg: "errorBg", + ":hover": { + opacity: 0.8 + } + // border: "1px solid", + // borderColor: "error", + // ":hover": { + // bg: "error", + // color: "static" + // } +}; + const dialog: ThemeUIStyleObject = { variant: "buttons.primary", color: "primary", @@ -136,7 +160,6 @@ const tool: ThemeUIStyleObject = { const statusItem: ThemeUIStyleObject = { variant: "buttons.icon", - p: 0, py: 1, px: 1 }; @@ -163,6 +186,10 @@ export const buttonVariants = { primary, secondary, tertiary, + + error, + errorSecondary, + list, anchor, tool,