From c1d47ad03cee600a05495d18490656374eed5eae Mon Sep 17 00:00:00 2001 From: Christian Chiarulli Date: Mon, 24 Jun 2024 21:41:10 -0400 Subject: [PATCH] release (#22) * feat: enable more editor settings and editor styling improvements #4 (#19) * feat: enable more editor settings and editor styling improvements #4 * feat: enable indent unit setting #4 * feat: enable more editor settings #4 (#20) * fix: wonky scrollbar styles, still need to actually solve this * feat: add client side validation for editor settings #4 (#21) * feat: add client side validation for editor settings #4 * feat: enable font family editor setting and refactor editor settings #4 * feat: add ability to save nsec * show npub * refactor: remove nostr settings, add profile and relays * dynamic relay form * update dialog name * fix: context menu holding stale state --------- Co-authored-by: Jason Chiarulli --- package-lock.json | 427 ++++++++++++++++++++ package.json | 2 + src-tauri/src/db/settings.rs | 23 +- src/components/context/Login.tsx | 14 +- src/components/settings/EditorSettings.tsx | 390 +++++++++++++++--- src/components/settings/NostrSettings.tsx | 70 ---- src/components/settings/ProfileSettings.tsx | 106 +++++ src/components/settings/RelaySettings.tsx | 99 +++++ src/components/tags/DeleteTagDialog.tsx | 8 +- src/components/ui/command.tsx | 153 +++++++ src/components/ui/popover.tsx | 31 ++ src/hooks/useCM6Editor.ts | 76 +++- src/hooks/useContextMenuEvent.ts | 141 ++++--- src/lib/editor/compartments.ts | 11 + src/lib/editor/utils.ts | 30 ++ src/lib/settings/constants.ts | 29 ++ src/lib/settings/utils.ts | 14 + src/pages/Settings.tsx | 28 +- src/store/index.ts | 18 +- src/styles/globals.css | 28 ++ src/types/index.ts | 26 +- src/types/settings.ts | 3 + src/validation/schemas.ts | 18 + 23 files changed, 1505 insertions(+), 240 deletions(-) delete mode 100644 src/components/settings/NostrSettings.tsx create mode 100644 src/components/settings/ProfileSettings.tsx create mode 100644 src/components/settings/RelaySettings.tsx create mode 100644 src/components/ui/command.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/lib/editor/compartments.ts create mode 100644 src/lib/editor/utils.ts create mode 100644 src/lib/settings/constants.ts create mode 100644 src/lib/settings/utils.ts create mode 100644 src/types/settings.ts create mode 100644 src/validation/schemas.ts diff --git a/package-lock.json b/package-lock.json index af3d37a4..3e8928b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -35,6 +36,7 @@ "@uiw/codemirror-themes": "^4.21.24", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "codemirror": "^6.0.1", "dayjs": "^1.11.10", "lucide-react": "^0.372.0", @@ -2180,6 +2182,418 @@ } } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.0.tgz", + "integrity": "sha512-2wdgj6eKNVoFNFtYv2xwkzhIJPlJ5L2aV0eKTZHi5dUVrGy+MhgoV8IyyeFpkZQrwwFzbFlnWl1bwyjVBCNapQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", + "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.0.tgz", + "integrity": "sha512-0tXZ5O6qAVvuN9SWP0X+zadHf9hzHiMf/vxOU+kXO+fbtS8lS57MXa6EmikDxk9s/Bmkk80+dcxgbvisIyeqxg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, + "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", @@ -3814,6 +4228,19 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", diff --git a/package.json b/package.json index 6af8e697..55d5e433 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", @@ -37,6 +38,7 @@ "@uiw/codemirror-themes": "^4.21.24", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "codemirror": "^6.0.1", "dayjs": "^1.11.10", "lucide-react": "^0.372.0", diff --git a/src-tauri/src/db/settings.rs b/src-tauri/src/db/settings.rs index 63b5b298..d93bb40e 100644 --- a/src-tauri/src/db/settings.rs +++ b/src-tauri/src/db/settings.rs @@ -7,13 +7,26 @@ pub fn insert_initial_settings(conn: &Connection) -> Result<()> { // theme ("theme", "dark"), // editor - ("vim", "true"), + ("vim", "false"), ("line_numbers", "false"), ("highlight_active_line", "false"), - ("line_wrapping", "false"), - // nostr - ("public_key", ""), - ("private_key", ""), + ("line_wrapping", "true"), + ("unordered_list_bullet", "*"), + ("indent_unit", "4"), + ("tab_size", "4"), + ("font_size", "16"), + ( + "font_family", + r#"SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace"#, + ), + ("font_weight", "normal"), + ("line_height", "1.5"), + // profile + ("npub", ""), + ("nsec", ""), + // relays + ("relays", "[\"relay.damus.io\", \"nos.lol\"]"), + ]; for (key, value) in initial_settings { diff --git a/src/components/context/Login.tsx b/src/components/context/Login.tsx index 04a78316..c6a775f2 100644 --- a/src/components/context/Login.tsx +++ b/src/components/context/Login.tsx @@ -2,7 +2,7 @@ import { GearIcon } from "@radix-ui/react-icons"; import { useAppContext } from "~/store"; export default function Login() { - const { setActivePage } = useAppContext(); + const { setActivePage, settings } = useAppContext(); const handleOpenSettings = () => { setActivePage("settings"); @@ -12,9 +12,15 @@ export default function Login() {
-

- Chris Chiarulli -

+ {settings.npub ? ( +

+ {settings.npub.slice(0, 8)}... +

+ ) : ( +

+ Login +

+ )}

logged in

{ + setEditorSettings({ + ...editorSettings, + selectedFontFamily: initialUserSelectedFontFamily(settings.font_family), + }); + }, []); + + async function updateSetting(key: string, value: string) { + if (key in settings) { + await setSetting(key, value); + setSettings({ ...settings, [key]: value }); + } + } async function handleSwitchOnClick( event: React.MouseEvent, - settingKey: SettingsSwitchKeys, + key: string, ) { if (event.target instanceof HTMLButtonElement) { setLoading(true); - const updatedSettings = { ...settings }; try { if (event.target.dataset.state === "unchecked") { - await setSetting(settingKey, "true"); - const getSettingResponse = await getSetting(settingKey); - if (getSettingResponse.data === "true") { - updatedSettings[settingKey] = getSettingResponse.data; - setSettings(updatedSettings); - } + await updateSetting(key, "true"); } else if (event.target.dataset.state === "checked") { - await setSetting(settingKey, "false"); - const getSettingResponse = await getSetting(settingKey); - if (getSettingResponse.data === "false") { - updatedSettings[settingKey] = getSettingResponse.data; - setSettings(updatedSettings); - } + await updateSetting(key, "false"); } } catch (error) { - console.error("Settings error: ", error); + console.error("Editor settings error: ", error); } finally { setLoading(false); } } } - async function handleInputOnChange( - event: React.ChangeEvent, + async function handleSelectOnValueChange( + key: string, + value: UnorderedListBullet | FontWeight, ) { - console.log("handleInputOnChange event: ", event); + setLoading(true); + try { + await updateSetting(key, value); + if (value === "-" || value === "*" || value === "+") { + setEditorSettings({ ...editorSettings, unorderedListBullet: value }); + } else if ( + value === "lighter" || + value === "normal" || + value === "bold" || + value === "bolder" + ) { + setEditorSettings({ ...editorSettings, fontWeight: value }); + } + } catch (error) { + console.error("Editor settings error: ", error); + } finally { + setLoading(false); + } + } + + async function handleInputOnBlur(key: string, value: string) { + setLoading(true); + try { + const validationResult = partialEditorSettingsSchema.safeParse({ + [key]: value, + }); + + if (validationResult.success) { + await updateSetting(key, value); + setErrorMessages({ ...errorMessages, [key]: "" }); + } else { + setErrorMessages({ + ...errorMessages, + [key]: validationResult.error.issues[0].message, + }); + } + } catch (error) { + console.error("Editor settings error: ", error); + } finally { + setLoading(false); + } + } + + async function handleComboxOnSelect(key: string, value: string) { + setLoading(true); + try { + await updateSetting(key, prioritizeUserFontFamilies(value)); + setEditorSettings({ + ...editorSettings, + selectedFontFamily: + value === editorSettings.selectedFontFamily ? "" : value, + }); + setOpenFontFamilyCombobox(false); + } catch (error) { + console.error("Editor settings error: ", error); + } finally { + setLoading(false); + } } return ( @@ -72,10 +175,10 @@ export default function EditorSettings() {
-
+
handleSwitchOnClick(event, "vim")} className="ml-2 disabled:cursor-pointer disabled:opacity-100" disabled={loading} @@ -86,10 +189,10 @@ export default function EditorSettings() {

-
+
handleSwitchOnClick(event, "line_numbers")} className="ml-2 disabled:cursor-pointer disabled:opacity-100" disabled={loading} @@ -100,10 +203,10 @@ export default function EditorSettings() {

-
+
handleSwitchOnClick(event, "highlight_active_line") } @@ -116,10 +219,10 @@ export default function EditorSettings() {

-
+
handleSwitchOnClick(event, "line_wrapping")} className="ml-2 disabled:cursor-pointer disabled:opacity-100" disabled={loading} @@ -131,9 +234,16 @@ export default function EditorSettings() {
- + handleSelectOnValueChange("unordered_list_bullet", value) + } + >
- +
@@ -148,64 +258,183 @@ export default function EditorSettings() {

- +
handleInputOnChange(event)} + placeholder="4" + className="disabled:cursor-text disabled:opacity-100" + disabled={loading} + min="0" + max="100" + value={editorSettings.indentUnit} + onChange={(event) => + setEditorSettings({ + ...editorSettings, + indentUnit: event.currentTarget.value, + }) + } + onBlur={() => + handleInputOnBlur("indent_unit", editorSettings.indentUnit) + } />

How many spaces a block should be indented

+ {errorMessages.indent_unit !== "" && ( +

+ {errorMessages.indent_unit} +

+ )}
- +
handleInputOnChange(event)} + className="disabled:cursor-text disabled:opacity-100" + disabled={loading} + min="0" + max="100" + value={editorSettings.tabSize} + onChange={(event) => + setEditorSettings({ + ...editorSettings, + tabSize: event.currentTarget.value, + }) + } + onBlur={() => + handleInputOnBlur("tab_size", editorSettings.tabSize) + } />

The width of the tab character

+ {errorMessages.tab_size !== "" && ( +

+ {errorMessages.tab_size} +

+ )}
- +
handleInputOnChange(event)} + placeholder="16" + className="disabled:cursor-text disabled:opacity-100" + disabled={loading} + min="1" + max="100" + value={editorSettings.fontSize} + onChange={(event) => + setEditorSettings({ + ...editorSettings, + fontSize: event.currentTarget.value, + }) + } + onBlur={() => + handleInputOnBlur("font_size", editorSettings.fontSize) + } />

Height in pixels of editor text

+ {errorMessages.font_size !== "" && ( +

+ {errorMessages.font_size} +

+ )}
- handleInputOnChange(event)} - /> + + + + + + + + + No font family found + + {comboboxPrioritizedFontFamilies.map((fontFamily) => ( + + handleComboxOnSelect("font_family", currentValue) + } + > + + {fontFamily.label} + + ))} + + + + +

The name of the font family used for editor text @@ -213,35 +442,70 @@ export default function EditorSettings() {

-
- handleInputOnChange(event)} - /> -
+

The weight of the font used for editor text

- +
handleInputOnChange(event)} + className="disabled:cursor-text disabled:opacity-100" + disabled={loading} + min="1" + max="10" + step="0.5" + value={editorSettings.lineHeight} + onChange={(event) => + setEditorSettings({ + ...editorSettings, + lineHeight: event.currentTarget.value, + }) + } + onBlur={() => + handleInputOnBlur("line_height", editorSettings.lineHeight) + } />

Height of editor lines, as a multiplier of font size

+ {errorMessages.line_height !== "" && ( +

+ {errorMessages.line_height} +

+ )}
diff --git a/src/components/settings/NostrSettings.tsx b/src/components/settings/NostrSettings.tsx deleted file mode 100644 index 927b0055..00000000 --- a/src/components/settings/NostrSettings.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "~/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "~/components/ui/card"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "~/components/ui/form"; -import { Input } from "~/components/ui/input"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -const FormSchema = z.object({ - nsec: z.string(), -}); - -export default function NostrSettings() { - const form = useForm>({ - resolver: zodResolver(FormSchema), - defaultValues: { - nsec: "", - }, - }); - - function onSubmit(data: z.infer) { - console.log(data); - } - - return ( - - - Nostr - - Enter your Nostr private key to enable Nostr features - - - -
- - ( - - Nostr Private Key - - - - Nostr private key - - - )} - /> - - - -
-
- ); -} diff --git a/src/components/settings/ProfileSettings.tsx b/src/components/settings/ProfileSettings.tsx new file mode 100644 index 00000000..82fc6194 --- /dev/null +++ b/src/components/settings/ProfileSettings.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { setSetting } from "~/api"; +import { Button } from "~/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form"; +import { Input } from "~/components/ui/input"; +import { useAppContext } from "~/store"; +import { getPublicKey, nip19 } from "nostr-tools"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const isValidNsec = (nsec: string) => { + try { + return nip19.decode(nsec).type === "nsec"; + } catch (e) { + return false; + } +}; + +const formSchema = z.object({ + nsec: z.string().refine(isValidNsec, { + message: "Invalid nsec.", + }), +}); + +export default function ProfileSettings() { + const [loading, setLoading] = useState(false); + + const { settings, setSettings } = useAppContext(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + nsec: settings.nsec || "", // Set the default value from settings + }, + }); + + // If settings.nsec is not available at the initial render, update the form state once settings are fetched + useEffect(() => { + if (settings.nsec) { + form.reset({ nsec: settings.nsec }); + } + }, [settings.nsec, form]); + + async function onSubmit(data: z.infer) { + setLoading(true); + const privateKey = nip19.decode(data.nsec).data as Uint8Array; + const publicKey = getPublicKey(privateKey); + const npub = nip19.npubEncode(publicKey); + const { nsec } = data; + await setSetting("nsec", nsec); + await setSetting("npub", npub); + setSettings({ ...settings, nsec: nsec, npub: npub }); + setLoading(false); + } + + return ( + + + Profile + + Enter your profile Details + + + +
+ + ( + + Secret Key + + + + Your secret key, keep this safe! + + + )} + /> + + + +
+
+ ); +} diff --git a/src/components/settings/RelaySettings.tsx b/src/components/settings/RelaySettings.tsx new file mode 100644 index 00000000..dfd5e5da --- /dev/null +++ b/src/components/settings/RelaySettings.tsx @@ -0,0 +1,99 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { Button } from "~/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "~/components/ui/form"; +import { Input } from "~/components/ui/input"; +import { useFieldArray, useForm } from "react-hook-form"; +import { z } from "zod"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../ui/card"; + +const profileFormSchema = z.object({ + urls: z + .array( + z.object({ + value: z.string().url({ message: "Please enter a valid URL." }), + }), + ) + .optional(), +}); + +type ProfileFormValues = z.infer; + +// This can come from your database or API. +const defaultValues: Partial = { + urls: [ + { value: "wss://relay.damus.io" }, + { value: "wss://nos.lol" }, + ], +}; + +export default function RelaySettings() { + const form = useForm({ + resolver: zodResolver(profileFormSchema), + defaultValues, + mode: "onChange", + }); + + const { fields, append } = useFieldArray({ + name: "urls", + control: form.control, + }); + + function onSubmit(data: ProfileFormValues) { + console.log(data); + } + + return ( + + + Relays + Configure your relays + + +
+ +
+ {fields.map((field, index) => ( + ( + + + + + + + )} + /> + ))} + +
+ +
+ +
+
+ ); +} diff --git a/src/components/tags/DeleteTagDialog.tsx b/src/components/tags/DeleteTagDialog.tsx index 697875ae..07c55424 100644 --- a/src/components/tags/DeleteTagDialog.tsx +++ b/src/components/tags/DeleteTagDialog.tsx @@ -49,12 +49,10 @@ export default function DeleteTagDialog() { - - Are you absolutely sure? {deleteTagDialogId} - + Are you sure? - This action cannot be undone. Are you sure you want to permanently - delete this tag? + This action cannot be undone. Are you sure you want to delete this + tag? diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 00000000..75864eae --- /dev/null +++ b/src/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { MagnifyingGlassIcon } from "@radix-ui/react-icons" +import { Command as CommandPrimitive } from "cmdk" + +import { cn } from "~/lib/utils" +import { Dialog, DialogContent } from "~/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 00000000..71a10875 --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "~/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/hooks/useCM6Editor.ts b/src/hooks/useCM6Editor.ts index c2bf8e60..40aa16de 100644 --- a/src/hooks/useCM6Editor.ts +++ b/src/hooks/useCM6Editor.ts @@ -1,24 +1,40 @@ import { useEffect, useRef, useState } from "react"; import { closeBrackets } from "@codemirror/autocomplete"; -import { history } from "@codemirror/commands"; +import { history, indentWithTab } from "@codemirror/commands"; import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; -import { bracketMatching, indentOnInput } from "@codemirror/language"; +import { + bracketMatching, + indentOnInput, + indentUnit, +} from "@codemirror/language"; import { languages } from "@codemirror/language-data"; import { EditorState } from "@codemirror/state"; import { crosshairCursor, drawSelection, dropCursor, + highlightActiveLine, highlightSpecialChars, + keymap, + // scrollPastEnd, lineNumbers, rectangularSelection, - // scrollPastEnd, } from "@codemirror/view"; import { vim } from "@replit/codemirror-vim"; import { useQueryClient } from "@tanstack/react-query"; import { getNote, updateNote } from "~/api"; import useThemeChange from "~/hooks/useThemeChange"; +import { + fontFamily, + fontSize, + fontWeight, + lineHeight, +} from "~/lib/editor/compartments"; +import { + customizeEditorThemeStyles, + indentUnitWhitespace, +} from "~/lib/editor/utils"; import { useAppContext } from "~/store"; import { EditorView } from "codemirror"; @@ -37,7 +53,7 @@ export const useCM6Editor = ({ initialDoc, onChange }: Props) => { const queryClient = useQueryClient(); - const theme = useThemeChange(); + const preferredTheme = useThemeChange(); async function handleBlur(view: EditorView) { const content = currentNote?.content; @@ -62,8 +78,19 @@ export const useCM6Editor = ({ initialDoc, onChange }: Props) => { useEffect(() => { if (!editorRef.current) return; + let theme; + if (preferredTheme === "dark") { + theme = darkTheme; + } else { + theme = lightTheme; + } + const extensions = [ - theme === "dark" ? darkTheme : lightTheme, + theme, + fontSize.of(theme), + fontFamily.of(theme), + fontWeight.of(theme), + lineHeight.of(theme), blurHandlerExtension, // highlightActiveLineGutter(), highlightSpecialChars(), @@ -80,7 +107,8 @@ export const useCM6Editor = ({ initialDoc, onChange }: Props) => { rectangularSelection(), crosshairCursor(), // scrollPastEnd(), - EditorView.lineWrapping, + keymap.of([indentWithTab]), + EditorState.tabSize.of(Number(settings.tab_size)), EditorState.readOnly.of(filter === "archived" || filter === "trashed"), EditorView.updateListener.of((update) => { if ( @@ -91,12 +119,11 @@ export const useCM6Editor = ({ initialDoc, onChange }: Props) => { } }), - // basicSetup, markdown({ base: markdownLanguage, codeLanguages: languages, - // addKeymap: true, }), + indentUnit.of(indentUnitWhitespace(settings.indent_unit)), ]; if (settings.vim === "true") { @@ -105,22 +132,49 @@ export const useCM6Editor = ({ initialDoc, onChange }: Props) => { if (settings.line_numbers === "true") { extensions.push(lineNumbers()); } + if (settings.highlight_active_line === "true") { + extensions.push(highlightActiveLine()); + } + if (settings.line_wrapping === "true") { + extensions.push(EditorView.lineWrapping); + } - const startState = EditorState.create({ + const initialState = EditorState.create({ doc: initialDoc, extensions, }); const view = new EditorView({ - state: startState, + state: initialState, parent: editorRef.current, }); + + customizeEditorThemeStyles(view, fontSize, "fontSize", settings.font_size); + customizeEditorThemeStyles( + view, + fontFamily, + "fontFamily", + settings.font_family, + ); + customizeEditorThemeStyles( + view, + fontWeight, + "fontWeight", + settings.font_weight, + ); + customizeEditorThemeStyles( + view, + lineHeight, + "lineHeight", + settings.line_height, + ); + setEditorView(view); return () => { view.destroy(); }; - }, [theme, currentNote?.id]); + }, [preferredTheme, currentNote?.id]); return { editorRef, editorView }; }; diff --git a/src/hooks/useContextMenuEvent.ts b/src/hooks/useContextMenuEvent.ts index b577da8e..a9132da3 100644 --- a/src/hooks/useContextMenuEvent.ts +++ b/src/hooks/useContextMenuEvent.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { listen } from "@tauri-apps/api/event"; @@ -12,85 +12,108 @@ import { export const useContextMenuEvent = () => { const queryClient = useQueryClient(); - const { currentNote, setCurrentNote, activeTag, setDeleteTagDialog, setDeleteTagDialogId } = - useAppContext(); - const [unlisten, setUnlisten] = useState<() => void>(() => () => {}); - - async function listenHandler() { - const app = useAppContext.getState(); + const [unlisten, setUnlisten] = useState<() => void | undefined>( + () => undefined, + ); + const listenHandler = useCallback(async () => { const unlisten = await listen("menu_event", (e) => { const payload = e.payload as ContextMenuEventPayload; const contextMenuEventKind = payload.contextMenuEventKind; const eventKey = Object.keys(contextMenuEventKind)[0]; + + const app = useAppContext.getState(); + + const handleNoteItemEvent = ( + noteItemEvent: NoteItemContextMenuEventPayload["NoteItem"], + ) => { + switch (noteItemEvent.eventKind) { + case "trash_note": + if (noteItemEvent.id === app.currentNote?.id) { + app.setCurrentNote(undefined); + } + void queryClient.invalidateQueries({ queryKey: ["notes"] }); + break; + default: + break; + } + }; + + const handleTagItemEvent = ( + tagItemEvent: TagItemContextMenuEventPayload["TagItem"], + ) => { + switch (tagItemEvent.eventKind) { + case "delete_tag": + const { id } = tagItemEvent; + app.setDeleteTagDialogId(id); + app.setDeleteTagDialog(true); + break; + default: + break; + } + }; + + const handleNoteTagItemEvent = ( + noteTagItemEvent: NoteTagItemContextMenuEventPayload["NoteTag"], + ) => { + switch (noteTagItemEvent.eventKind) { + case "untag_note": + const { tagId } = noteTagItemEvent; + const filteredTags = app.currentNote?.tags.filter( + (tag) => !(tag.id === tagId), + ); + + if (tagId === app.activeTag?.id) { + void queryClient.invalidateQueries({ queryKey: ["notes"] }); + } + + if (app.currentNote?.tags && filteredTags) { + if (app.currentNote) { + app.setCurrentNote({ ...app.currentNote, tags: filteredTags }); + } + } + break; + default: + break; + } + }; + switch (eventKey) { case "NoteItem": - const noteItemContextMenuEventPayload = - contextMenuEventKind as NoteItemContextMenuEventPayload; - const noteItemEvent = noteItemContextMenuEventPayload.NoteItem; - switch (noteItemEvent.eventKind) { - case "trash_note": - if (noteItemEvent.id === currentNote?.id) { - setCurrentNote(undefined); - } - void queryClient.invalidateQueries({ queryKey: ["notes"] }); - break; - default: - break; - } + handleNoteItemEvent( + (contextMenuEventKind as NoteItemContextMenuEventPayload).NoteItem, + ); break; case "TagItem": - const tagItemContextMenuEventPayload = - contextMenuEventKind as TagItemContextMenuEventPayload; - const tagItemEvent = tagItemContextMenuEventPayload.TagItem; - switch (tagItemEvent.eventKind) { - case "delete_tag": - const { id } = tagItemEvent; - setDeleteTagDialogId(id); - setDeleteTagDialog(true); - break; - default: - break; - } + handleTagItemEvent( + (contextMenuEventKind as TagItemContextMenuEventPayload).TagItem, + ); break; - case "NoteTag": - const noteTagItemContextMenuEventPayload = - contextMenuEventKind as NoteTagItemContextMenuEventPayload; - const noteTagItemEvent = noteTagItemContextMenuEventPayload.NoteTag; - switch (noteTagItemEvent.eventKind) { - case "untag_note": - const { tagId } = noteTagItemEvent; - - const filteredTags = currentNote?.tags.filter( - (tag) => !(tag.id === tagId), - ); - - if (tagId === activeTag?.id) { - void queryClient.invalidateQueries({ queryKey: ["notes"] }); - } - - if (currentNote?.tags && filteredTags) { - if (currentNote) { - app.setCurrentNote({ ...currentNote, tags: filteredTags }); - } - } - break; - default: - break; - } + handleNoteTagItemEvent( + (contextMenuEventKind as NoteTagItemContextMenuEventPayload) + .NoteTag, + ); break; default: break; } }); + setUnlisten(() => unlisten); - } + }, [queryClient]); useEffect(() => { if (unlisten) { unlisten(); } + void listenHandler(); - }, [currentNote, activeTag]); + + return () => { + if (unlisten) { + unlisten(); + } + }; + }, [listenHandler, unlisten]); }; diff --git a/src/lib/editor/compartments.ts b/src/lib/editor/compartments.ts new file mode 100644 index 00000000..7ea1db48 --- /dev/null +++ b/src/lib/editor/compartments.ts @@ -0,0 +1,11 @@ +import { Compartment } from "@codemirror/state"; + +const fontSize = new Compartment(); + +const fontFamily = new Compartment(); + +const fontWeight = new Compartment(); + +const lineHeight = new Compartment(); + +export { fontSize, fontFamily, fontWeight, lineHeight }; diff --git a/src/lib/editor/utils.ts b/src/lib/editor/utils.ts new file mode 100644 index 00000000..8778dad8 --- /dev/null +++ b/src/lib/editor/utils.ts @@ -0,0 +1,30 @@ +import { type Compartment } from "@codemirror/state"; +import { EditorView } from "codemirror"; + +export function customizeEditorThemeStyles( + view: EditorView, + compartment: Compartment, + className: string, + setting: string, +) { + if (className === "fontSize") { + setting = setting + "px"; + } + + const theme = EditorView.theme({ + ".cm-content": { + [className]: setting, + }, + ".cm-gutters": { + [className]: setting, + }, + }); + + view.dispatch({ + effects: compartment.reconfigure(theme), + }); +} + +export function indentUnitWhitespace(indentUnitSetting: string) { + return " ".repeat(Number(indentUnitSetting)); +} diff --git a/src/lib/settings/constants.ts b/src/lib/settings/constants.ts new file mode 100644 index 00000000..8538d0cd --- /dev/null +++ b/src/lib/settings/constants.ts @@ -0,0 +1,29 @@ +export const prioritizedFontFamilies = + 'SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace'; + +export const comboboxPrioritizedFontFamilies = [ + { + value: "SFMono-Regular", + label: "SFMono-Regular", + }, + { + value: "Consolas", + label: "Consolas", + }, + { + value: '"Liberation Mono"', + label: "Liberation Mono", + }, + { + value: "Menlo", + label: "Menlo", + }, + { + value: "Courier", + label: "Courier", + }, + { + value: "monospace", + label: "monospace", + }, +]; diff --git a/src/lib/settings/utils.ts b/src/lib/settings/utils.ts new file mode 100644 index 00000000..da0d97f1 --- /dev/null +++ b/src/lib/settings/utils.ts @@ -0,0 +1,14 @@ +import { prioritizedFontFamilies } from "./constants"; + +export function initialUserSelectedFontFamily(fontFamilies: string) { + return fontFamilies.split(", ")[0]; +} + +export function prioritizeUserFontFamilies(value: string) { + let userPrioritizedFontFamilies = prioritizedFontFamilies.split(", "); + userPrioritizedFontFamilies = userPrioritizedFontFamilies.filter( + (fontFamily) => fontFamily !== value, + ); + userPrioritizedFontFamilies.unshift(value); + return userPrioritizedFontFamilies.join(", "); +} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 179251b7..ba173d42 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -2,10 +2,11 @@ import { useState } from "react"; import EditorSettings from "~/components/settings/EditorSettings"; import GeneralSettings from "~/components/settings/GeneralSettings"; -import NostrSettings from "~/components/settings/NostrSettings"; +import ProfileSettings from "~/components/settings/ProfileSettings"; +import RelaySettings from "~/components/settings/RelaySettings"; import ThemeSettings from "~/components/settings/ThemeSettings"; -type Tab = "General" | "Editor" | "Theme" | "Nostr"; +type Tab = "General" | "Editor" | "Theme" | "Profile" | "Relays"; export default function Settings() { const [currentTab, setCurrentTab] = useState("General"); @@ -15,9 +16,9 @@ export default function Settings() { }; return ( -
-

Settings

-
+
+

Settings

+
-
+
{currentTab === "General" && } {currentTab === "Editor" && } {currentTab === "Theme" && } - {currentTab === "Nostr" && } + {currentTab === "Profile" && } + {currentTab === "Relays" && }
diff --git a/src/store/index.ts b/src/store/index.ts index 66d1725b..c8cbd3ed 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -66,7 +66,23 @@ export const useAppContext = create()( setNoteFeedScrollPosition: (noteFeedScrollPosition) => set({ noteFeedScrollPosition }), - settings: {}, + settings: { + theme: "dark", + vim: "false", + line_numbers: "false", + highlight_active_line: "false", + line_wrapping: "true", + unordered_list_bullet: "*", + indent_unit: "", + tab_size: "", + font_size: "", + font_family: "", + font_weight: "normal", + line_height: "", + nsec: "", + npub: "", + relays: "[\"relay.damus.io\", \"nos.lol\"]", + }, setSettings: (settings) => set({ settings }), deleteTagDialog: false, diff --git a/src/styles/globals.css b/src/styles/globals.css index 20cbc685..44cf26d2 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -91,3 +91,31 @@ padding-bottom: 10rem; } } + +/* TODO: fix this so that the shadcn scroll area bar can be used */ + +/* Scrollbar background */ +::-webkit-scrollbar { + @apply bg-background; +} + +/* Scrollbar thumb (foreground) */ +::-webkit-scrollbar-thumb { + @apply bg-primary/10; + @apply rounded-full; +} + +/* Scrollbar thumb on hover */ +::-webkit-scrollbar-thumb:hover { + @apply bg-primary/15; +} + +/* Optional: Scrollbar width */ +::-webkit-scrollbar { + @apply w-2.5; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; +} diff --git a/src/types/index.ts b/src/types/index.ts index 804b41ab..65bb406c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -84,17 +84,19 @@ export type Settings = { // theme theme?: "light" | "dark"; // editor - vim?: "true" | "false"; - line_numbers?: "true" | "false"; - highlight_active_line?: "true" | "false"; - line_wrapping?: "true" | "false"; + vim: "true" | "false"; + line_numbers: "true" | "false"; + highlight_active_line: "true" | "false"; + line_wrapping: "true" | "false"; + unordered_list_bullet: "-" | "*" | "+"; + indent_unit: string; + tab_size: string; + font_size: string; + font_family: string; + font_weight: "lighter" | "normal" | "bold" | "bolder"; + line_height: string; // nostr - public_key?: string; - private_key?: string; + nsec: string; + npub: string; + relays: string; }; - -export type SettingsSwitchKeys = - | "vim" - | "line_numbers" - | "highlight_active_line" - | "line_wrapping" diff --git a/src/types/settings.ts b/src/types/settings.ts new file mode 100644 index 00000000..cb709533 --- /dev/null +++ b/src/types/settings.ts @@ -0,0 +1,3 @@ +export type UnorderedListBullet = "-" | "*" | "+"; + +export type FontWeight = "lighter" | "normal" | "bold" | "bolder"; diff --git a/src/validation/schemas.ts b/src/validation/schemas.ts new file mode 100644 index 00000000..808175cb --- /dev/null +++ b/src/validation/schemas.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const editorSettingsSchema = z.object({ + indent_unit: z.string().regex(/^([0-9]|[1-9][0-9]|100)$/, { + message: "Must be an integer from 0 to 100", + }), + tab_size: z.string().regex(/^([0-9]|[1-9][0-9]|100)$/, { + message: "Must be an integer from 0 to 100", + }), + font_size: z.string().regex(/^([1-9]|[1-9][0-9]|100)$/, { + message: "Must be an integer from 1 to 100", + }), + line_height: z.string().regex(/^(([1-9](\.[0-9])?)|(10(\.0)?))$/, { + message: "Must be a number with one decimal from 1.0 to 10.0", + }), +}); + +export const partialEditorSettingsSchema = editorSettingsSchema.partial();