diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..15aa299 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2026 Marcello Brito + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the β€œSoftware”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED β€œAS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2739a66 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +please don't use this +I made this to test things + +I am not to be trusted under any circunstances \ No newline at end of file diff --git a/bun.lock b/bun.lock index fe05459..5cac9c4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,66 +1,67 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "sipher", "dependencies": { - "@better-fetch/fetch": "^1.1.21", - "@convex-dev/better-auth": "^0.10.9", - "@marsidev/react-turnstile": "^1.4.0", - "@matrix-org/olm": "^3.2.15", - "@nanostores/react": "^1.0.0", - "@phosphor-icons/react": "^2.1.10", - "@radix-ui/react-avatar": "^1.1.11", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-context-menu": "^2.2.16", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-hover-card": "^1.1.15", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-menubar": "^1.1.16", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-tooltip": "^1.2.8", - "@types/libsodium-wrappers": "^0.7.14", - "better-auth": "1.4.9", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "convex": "^1.31.2", - "cross-env": "^10.1.0", - "date-fns": "^4.1.0", - "dexie": "^4.2.1", - "dexie-react-hooks": "^4.2.0", - "framer-motion": "^12.23.26", - "libsodium-wrappers": "^0.7.15", - "lucide-react": "^0.562.0", - "nanostores": "^1.1.0", - "next": "16.1.1", - "next-themes": "^0.4.6", - "react": "19.2.3", - "react-day-picker": "^9.13.0", - "react-dom": "19.2.3", - "socket.io": "^4.8.3", - "socket.io-client": "^4.8.3", - "sonner": "^2.0.7", - "tailwind-merge": "^3.4.0", - "ws": "^8.18.3", - "zod": "^4.2.1", + "@better-fetch/fetch": "latest", + "@convex-dev/better-auth": "latest", + "@marsidev/react-turnstile": "latest", + "@matrix-org/olm": "latest", + "@nanostores/react": "latest", + "@phosphor-icons/react": "latest", + "@radix-ui/react-avatar": "latest", + "@radix-ui/react-checkbox": "latest", + "@radix-ui/react-context-menu": "latest", + "@radix-ui/react-dialog": "latest", + "@radix-ui/react-hover-card": "latest", + "@radix-ui/react-label": "latest", + "@radix-ui/react-menubar": "latest", + "@radix-ui/react-popover": "latest", + "@radix-ui/react-progress": "latest", + "@radix-ui/react-scroll-area": "latest", + "@radix-ui/react-separator": "latest", + "@radix-ui/react-slot": "latest", + "@radix-ui/react-tooltip": "latest", + "@types/libsodium-wrappers": "latest", + "better-auth": "latest", + "class-variance-authority": "latest", + "clsx": "latest", + "cmdk": "latest", + "convex": "latest", + "cross-env": "latest", + "date-fns": "latest", + "dexie": "latest", + "dexie-react-hooks": "latest", + "framer-motion": "latest", + "libsodium-wrappers": "latest", + "lucide-react": "latest", + "nanostores": "latest", + "next": "latest", + "next-themes": "latest", + "react": "latest", + "react-day-picker": "latest", + "react-dom": "latest", + "socket.io": "latest", + "socket.io-client": "latest", + "sonner": "latest", + "tailwind-merge": "latest", + "ws": "latest", + "zod": "latest", }, "devDependencies": { - "@tailwindcss/postcss": "^4.1.18", - "@types/bun": "^1.3.5", - "@types/node": "^25.0.3", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "@types/ws": "^8.18.1", - "babel-plugin-react-compiler": "1.0.0", - "tailwindcss": "^4.1.18", - "tsx": "^4.21.0", - "tw-animate-css": "^1.4.0", - "typescript": "^5.9.3", + "@tailwindcss/postcss": "latest", + "@types/bun": "latest", + "@types/node": "latest", + "@types/react": "latest", + "@types/react-dom": "latest", + "@types/ws": "latest", + "babel-plugin-react-compiler": "latest", + "tailwindcss": "latest", + "tsx": "latest", + "tw-animate-css": "latest", + "typescript": "latest", }, }, }, @@ -73,11 +74,11 @@ "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@better-auth/core": ["@better-auth/core@1.4.9", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-JT2q4NDkQzN22KclUEoZ7qU6tl9HUTfK1ctg2oWlT87SEagkwJcnrUwS9VznL+u9ziOIfY27P0f7/jSnmvLcoQ=="], + "@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], "@better-auth/passkey": ["@better-auth/passkey@1.4.9", "", { "dependencies": { "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/core": "1.4.9", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-auth": "1.4.9", "better-call": "1.1.7", "nanostores": "^1.0.1" } }, "sha512-fPsV0LYbmPytxrTaltM2RXbJnmSttX9UWr4wkZtJYgCBGeFqN8+8ZzBTZXOymWDJTVQ0kVZrD7c7/HyxXEG1zA=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.9", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.9" } }, "sha512-Tthy1/Gmx+pYlbvRQPBTKfVei8+pJwvH1NZp+5SbhwA6K2EXIaoonx/K6N/AXYs2aKUpyR4/gzqDesDjL7zd6A=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], @@ -215,7 +216,7 @@ "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], - "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.4.0", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-3aR7mh4lATeayWt6GjWuYyLjM0GL148z7/ZQl0rLKGpDYIrWgoU2PYsdAdA9fzH+JysW3Q2OaPfHvv66cwcAZg=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.4.1", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-1jE0IjvB8z+q1NFRs3149gXzXwIzXQWqQjn9fmAr13BiE3RYLWck5Me6flHYE90shW5L12Jkm6R1peS1OnA9oQ=="], "@matrix-org/olm": ["@matrix-org/olm@3.2.15", "", {}, "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="], @@ -417,7 +418,7 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], - "better-auth": ["better-auth@1.4.9", "", { "dependencies": { "@better-auth/core": "1.4.9", "@better-auth/telemetry": "1.4.9", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-usSdjuyTzZwIvM8fjF8YGhPncxV3MAg3dHUO9uPUnf0yklXUSYISiH1+imk6/Z+UBqsscyyPRnbIyjyK97p7YA=="], + "better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], "better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="], @@ -475,7 +476,7 @@ "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - "framer-motion": ["framer-motion@12.23.26", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA=="], + "framer-motion": ["framer-motion@12.23.27", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-EAcX8FS8jzZ4tSKpj+1GhwbVY+r1gfamPFwXZAsioPqu/ffRwU2otkKg6GEDCR41FVJv3RoBN7Aqep6drL9Itg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -493,9 +494,9 @@ "kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="], - "libsodium": ["libsodium@0.7.15", "", {}, "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw=="], + "libsodium": ["libsodium@0.7.16", "", {}, "sha512-3HrzSPuzm6Yt9aTYCDxYEG8x8/6C0+ag655Y7rhhWZM9PT4NpdnbqlzXhGZlDnkgR6MeSTnOt/VIyHLs9aSf+Q=="], - "libsodium-wrappers": ["libsodium-wrappers@0.7.15", "", { "dependencies": { "libsodium": "^0.7.15" } }, "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ=="], + "libsodium-wrappers": ["libsodium-wrappers@0.7.16", "", { "dependencies": { "libsodium": "^0.7.16" } }, "sha512-Gtr/WBx4dKjvRL1pvfwZqu7gO6AfrQ0u9vFL+kXihtHf6NfkROR8pjYWn98MFDI3jN19Ii1ZUfPR9afGiPyfHg=="], "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], @@ -639,7 +640,13 @@ "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], - "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + + "@better-auth/passkey/@better-auth/core": ["@better-auth/core@1.4.9", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-JT2q4NDkQzN22KclUEoZ7qU6tl9HUTfK1ctg2oWlT87SEagkwJcnrUwS9VznL+u9ziOIfY27P0f7/jSnmvLcoQ=="], + + "@better-auth/passkey/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + + "@convex-dev/better-auth/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 11ebd0f..c4792ab 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -951,6 +951,7 @@ export declare const components: { "query", "internal", { + join?: any; limit?: number; model: | "user" @@ -1004,6 +1005,7 @@ export declare const components: { "query", "internal", { + join?: any; model: | "user" | "userStatus" @@ -2006,6 +2008,12 @@ export declare const components: { }; olm: { index: { + consumeOTK: FunctionReference< + "mutation", + "internal", + { keyId: string; userId: string }, + any + >; retrieveServerOlmAccount: FunctionReference< "query", "internal", diff --git a/convex/auth.ts b/convex/auth.ts index 69f82e8..fa88315 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -194,4 +194,17 @@ export const getParticipantDetails = query({ participantIds: args.participantIds, }); }, +}); + +export const consumeOTK = mutation({ + args: { + userId: v.string(), + keyId: v.string(), + }, + handler: async (ctx, args) => { + return ctx.runMutation(components.betterAuth.olm.index.consumeOTK, { + userId: args.userId, + keyId: args.keyId, + }); + }, }); \ No newline at end of file diff --git a/convex/betterAuth/_generated/component.ts b/convex/betterAuth/_generated/component.ts index bb6e713..c5c49be 100644 --- a/convex/betterAuth/_generated/component.ts +++ b/convex/betterAuth/_generated/component.ts @@ -927,6 +927,7 @@ export type ComponentApi = "query", "internal", { + join?: any; limit?: number; model: | "user" @@ -981,6 +982,7 @@ export type ComponentApi = "query", "internal", { + join?: any; model: | "user" | "userStatus" @@ -1986,6 +1988,13 @@ export type ComponentApi = }; olm: { index: { + consumeOTK: FunctionReference< + "mutation", + "internal", + { keyId: string; userId: string }, + any, + Name + >; retrieveServerOlmAccount: FunctionReference< "query", "internal", diff --git a/convex/betterAuth/olm/index.ts b/convex/betterAuth/olm/index.ts index e3b7f10..68bceea 100644 --- a/convex/betterAuth/olm/index.ts +++ b/convex/betterAuth/olm/index.ts @@ -45,4 +45,29 @@ export const retrieveServerOlmAccount = query({ return null; }, -}); \ No newline at end of file +}); + +export const consumeOTK = mutation({ + args: { + userId: v.string(), + keyId: v.string(), + }, + handler: async (ctx, args) => { + const olmAccount = await ctx.db.get<"olmAccount">(args.userId as Id<"olmAccount">); + if (!olmAccount) throw new Error("User has no OLM account"); + + const oneTimeKeys = olmAccount.oneTimeKeys; + const keyIndex = oneTimeKeys.findIndex((key) => key.keyId === args.keyId); + if (keyIndex === -1) throw new Error("The key to be consumed was not found"); + + oneTimeKeys.splice(keyIndex, 1); + await ctx.db.patch<"olmAccount">(args.userId as Id<"olmAccount">, { + oneTimeKeys, + }); + + return { + consumed: true, + keysLeft: oneTimeKeys.length + } + }, +}) \ No newline at end of file diff --git a/convex/betterAuth/user/index.ts b/convex/betterAuth/user/index.ts index 3703928..599f035 100644 --- a/convex/betterAuth/user/index.ts +++ b/convex/betterAuth/user/index.ts @@ -353,6 +353,7 @@ export const getParticipantDetails = query({ const participantDetails = await Promise.all(filteredParticipantIds.map(async (id) => { const participant = await ctx.db.get("user", id) const participantStatus = await ctx.db.query("userStatus").withIndex("userId", (q) => q.eq("userId", id)).first(); + const participantOlmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", id)).first(); if (!participant) return null; return { @@ -362,9 +363,10 @@ export const getParticipantDetails = query({ displayUsername: participant.displayUsername, image: participant.image, status: participantStatus?.status || "offline", + olmAccount: participantOlmAccount, } })); - return participantDetails.filter(Boolean); + return participantDetails } }) \ No newline at end of file diff --git a/package.json b/package.json index 517ecad..75157a3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "@better-fetch/fetch": "^1.1.21", "@convex-dev/better-auth": "^0.10.9", - "@marsidev/react-turnstile": "^1.4.0", + "@marsidev/react-turnstile": "^1.4.1", "@matrix-org/olm": "^3.2.15", "@nanostores/react": "^1.0.0", "@phosphor-icons/react": "^2.1.10", @@ -30,7 +30,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@types/libsodium-wrappers": "^0.7.14", - "better-auth": "1.4.9", + "better-auth": "1.4.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -39,8 +39,8 @@ "date-fns": "^4.1.0", "dexie": "^4.2.1", "dexie-react-hooks": "^4.2.0", - "framer-motion": "^12.23.26", - "libsodium-wrappers": "^0.7.15", + "framer-motion": "^12.23.27", + "libsodium-wrappers": "^0.7.16", "lucide-react": "^0.562.0", "nanostores": "^1.1.0", "next": "16.1.1", @@ -53,7 +53,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "ws": "^8.18.3", - "zod": "^4.2.1" + "zod": "^4.3.5" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", diff --git a/src/app/auth/scripts/makeKeys.ts b/src/app/auth/scripts/makeKeys.ts index 9394f59..a6fade4 100644 --- a/src/app/auth/scripts/makeKeys.ts +++ b/src/app/auth/scripts/makeKeys.ts @@ -1,7 +1,7 @@ import { db } from "@/lib/db"; // Load OLM via script tag to bypass bundler entirely -async function loadOlm() { +export async function loadOlm() { if (typeof window === "undefined") throw new Error("OLM requires browser"); if ((window as any).Olm) return (window as any).Olm; @@ -64,6 +64,19 @@ export default async function makeKeysOnSignUp( const pickledAccount = account.pickle(localPassword); + // Store password in sessionStorage for unpickling later + sessionStorage.setItem(`olm_password_${odId}`, localPassword); + + // Cache the account in window + if (!(window as any).olmAccountCache) { + (window as any).olmAccountCache = {}; + } + (window as any).olmAccountCache[odId] = account; + + // Set the OLM session into the window object + (window as any).olmSession = new Olm.Session(); + + // Store the olm account on DB await db.olmAccounts.put({ odId, pickledAccount, diff --git a/src/components/app-container.tsx b/src/components/app-container.tsx index da1eddd..cda0aa0 100644 --- a/src/components/app-container.tsx +++ b/src/components/app-container.tsx @@ -5,16 +5,17 @@ import OlmSetupDialog from "@/components/olm/olm-setup-dialog"; import { MainContentLayout } from "@/components/ui/layout"; import { Spinner } from "@/components/ui/spinner"; import UserFloatingCard from "@/components/ui/user/floating-card"; -import { useOlmSetup } from "@/hooks/use-olm-setup"; -import { useSocket } from "@/hooks/use-socket"; +import { OlmProvider, useOlmContext } from "@/contexts/olm-context"; +import { SocketProvider, useSocketContext } from "@/contexts/socket-context"; import { authClient } from "@/lib/auth/client"; import { getRandomPhrase, type PhrasePreference } from "@/lib/constants/phrases"; import { useMutation, useQuery } from "convex/react"; import { redirect, useParams, usePathname } from "next/navigation"; import { useCallback, useEffect, useMemo } from "react"; import { api } from "../../convex/_generated/api"; +import OlmPasswordDialog from "./olm/olm-password-dialog"; -export default function AppContainer() { +function AppContainerContent() { const pathname = usePathname(); const params = useParams(); @@ -36,29 +37,15 @@ export default function AppContainer() { } return { type: 'home' as const }; }, [pathname, params]); - const { data, error, isPending, refetch } = authClient.useSession(); - const hasServerOlm = useQuery( - api.auth.retrieveServerOlmAccount, - data?.user?.id ? { userId: data.user.id } : "skip" - ); + const { data } = authClient.useSession(); - const userStatus = useQuery(api.auth.getUserStatus); - const { socketStatus, socketInfo, disconnect, connect } = useSocket({ - user: { - id: data?.user?.id, - status: userStatus ? { - status: userStatus.status, - isUserSet: userStatus.isUserSet, - } : { - status: "offline" as const, - isUserSet: false, - }, - }, - refetchUser: refetch - }); + // Use socket context instead of hook + const { socketStatus, socketInfo, disconnect, connect } = useSocketContext(); + + // Use OLM context + const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmContext(); - const sendKeysToServer = useMutation(api.auth.sendKeysToServer); const updateUserMetadata = useMutation(api.auth.updateUserMetadata); useEffect(() => { @@ -70,29 +57,11 @@ export default function AppContainer() { } }, [data, updateUserMetadata]); - const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmSetup({ - userId: data?.user?.id, - hasServerOlm, - sendKeysToServer - }); - const getPhrase = useCallback(() => { const preference = data?.user?.metadata?.phrasePreference as PhrasePreference | undefined; return getRandomPhrase(preference); }, [data?.user?.metadata?.phrasePreference]); - if (isPending) { - return ( -
- -
- ); - } - - if (error || !data) { - return redirect(`/auth${error ? `?error=${error.cause}` : "?error=no-data"}`); - } - if (["connecting", "error", "disconnected"].includes(socketStatus)) { return (
@@ -101,6 +70,10 @@ export default function AppContainer() { ); } + if (!data?.user) { + return null; + } + return ( <> @@ -121,6 +94,7 @@ export default function AppContainer() { /> + + +
+ ); + } + + if (error || !data) { + return redirect(`/auth${error ? `?error=${error.cause}` : "?error=no-data"}`); + } + + return ( + + + + + + + ); +} + diff --git a/src/components/home/index.tsx b/src/components/home/index.tsx index b4e3f6d..69e557d 100644 --- a/src/components/home/index.tsx +++ b/src/components/home/index.tsx @@ -83,7 +83,7 @@ export default function AppSidebar({ children, socketStatus, socketInfo, current -
+
@@ -124,8 +124,8 @@ export default function AppSidebar({ children, socketStatus, socketInfo, current
{/* Spacer for centering on mobile */}
- -
+ +
{children}
diff --git a/src/components/olm/olm-password-dialog.tsx b/src/components/olm/olm-password-dialog.tsx new file mode 100644 index 0000000..f3cd12a --- /dev/null +++ b/src/components/olm/olm-password-dialog.tsx @@ -0,0 +1,100 @@ +import { useOlmContext } from "@/contexts/olm-context"; +import { Info, KeyRound, ShieldCheck } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Button } from "../ui/button"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog"; +import { Input } from "../ui/input"; + +export default function OlmPasswordDialog({ userId }: { userId: string }) { + const [needsPassword, setNeedsPassword] = useState(false); + const [password, setPasswordInput] = useState(""); + const { setPassword } = useOlmContext(); + + useEffect(() => { + // Get the password from the session storage + const password = sessionStorage.getItem(`olm_password_${userId}`); + console.log("πŸ”’ Password from session storage:", password); + if (!password) { + setNeedsPassword(true); + return; + } + + setPassword(password); + setNeedsPassword(false); + }, [userId]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (password.trim()) { + setPassword(password); + setNeedsPassword(false); + } + }; + + return ( + + e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()}> + +
+
+ +
+
+ + Encryption Password Required + + + Enter your encryption password to access this conversation. This may be different from your login password. + +
+
+
+ +
+
+ setPasswordInput(e.target.value)} + /> +
+
+ +

+ Your password is stored locally and never sent to our servers. +

+
+
+ +

+ You'll be asked to re-enter this password each time you start a new browser session. +

+
+
+
+ +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/ui/dm/DmChannelContent.tsx b/src/components/ui/dm/DmChannelContent.tsx new file mode 100644 index 0000000..a61834c --- /dev/null +++ b/src/components/ui/dm/DmChannelContent.tsx @@ -0,0 +1,355 @@ +import { useOlmContext } from "@/contexts/olm-context"; +import { useSocketContext } from "@/contexts/socket-context"; +import { clearUnread, db, sendMessage } from "@/lib/db"; +import { useLiveQuery } from "dexie-react-hooks"; +import { KeyRound } from "lucide-react"; +import React, { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { Avatar, AvatarFallback, AvatarImage } from "../avatar"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../dialog"; +import { Input } from "../input"; + +interface DMChannelContentProps { + userId: string + channelId: string + participantDetails: SiPher.ParticipantDetail[] +} + +export default function DMChannelContent( + { + userId, + channelId, + participantDetails, + }: DMChannelContentProps +) { + const otherUser = useMemo(() => participantDetails[0], [participantDetails]); + const [olmSession, setOlmSession] = useState(null); + const [sessionError, setSessionError] = useState(null); + const [messageInput, setMessageInput] = useState(""); + const [messageLimit, setMessageLimit] = useState(50); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const { sendMessage: sendMessageToServer } = useSocketContext(); + const { olmAccount, password, isReady, getSession } = useOlmContext(); + const messagesEndRef = React.useRef(null); + const scrollContainerRef = React.useRef(null); + const prevScrollHeightRef = React.useRef(0); + + // Get total message count + const totalMessageCount = useLiveQuery( + () => db.messages.where("channelId").equals(channelId).count(), + [channelId] + ) ?? 0; + + // Get messages from the local database with pagination + const allMessages = useLiveQuery( + () => db.messages.where("channelId").equals(channelId).sortBy("timestamp"), + [channelId] + ) ?? []; + + // Take only the most recent messages based on limit + const messages = useMemo(() => { + return allMessages.slice(-messageLimit); + }, [allMessages, messageLimit]); + + const hasMoreMessages = messages.length < totalMessageCount; + + // Reset message limit when channel changes + useEffect(() => { + setMessageLimit(50); + }, [channelId]); + + // Handle scroll to load more messages + const handleScroll = React.useCallback(async (e: React.UIEvent) => { + const target = e.currentTarget; + const scrollTop = target.scrollTop; + + // If scrolled near the top (within 100px) and there are more messages + if (scrollTop < 100 && hasMoreMessages && !isLoadingMore) { + setIsLoadingMore(true); + + // Save current scroll height + prevScrollHeightRef.current = target.scrollHeight; + + // Load 50 more messages + await new Promise(resolve => setTimeout(resolve, 100)); // Small delay to avoid rapid firing + setMessageLimit(prev => prev + 50); + + setIsLoadingMore(false); + } + }, [hasMoreMessages, isLoadingMore]); + + // Preserve scroll position after loading more messages + useEffect(() => { + if (prevScrollHeightRef.current > 0 && scrollContainerRef.current) { + const newScrollHeight = scrollContainerRef.current.scrollHeight; + const scrollDiff = newScrollHeight - prevScrollHeightRef.current; + scrollContainerRef.current.scrollTop += scrollDiff; + prevScrollHeightRef.current = 0; + } + }, [messages.length]); + + // Scroll to bottom on initial load / channel change (instant) + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "auto" }); + }, [channelId]); + + // Auto-scroll to bottom when new messages arrive (smooth) + useEffect(() => { + if (messages.length > 0 && scrollContainerRef.current) { + const container = scrollContainerRef.current; + const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 200; + + // Only auto-scroll if user is near the bottom + if (isNearBottom) { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + } + } + }, [allMessages.length]); + + // Clear unread count when entering the channel + useEffect(() => { + clearUnread(channelId); + console.debug("[DMChannelContent] Cleared unread count for channel", channelId); + }, [channelId]); + + // Guard: Check if otherUser exists + if (!otherUser) { + return ( +
+
+

Loading participant information...

+
+
+ ); + } + + // Get or create session when OLM is ready and we have the other user's account + useEffect(() => { + const loadSession = async () => { + if (!isReady || !olmAccount || !otherUser || !otherUser.olmAccount) { + return; + } + + setSessionError(null); + + try { + const session = await getSession(otherUser.id, { + identityKey: otherUser.olmAccount.identityKey, + oneTimeKeys: otherUser.olmAccount.oneTimeKeys, + }); + + if (session) { + setOlmSession(session); + } else { + setSessionError("Failed to create encryption session"); + } + } catch (err) { + console.error("[DMChannelContent] Failed to get session:", err); + setSessionError(err instanceof Error ? err.message : "Unknown error"); + } + }; + + loadSession(); + }, [isReady, olmAccount, otherUser, password, getSession]) + + // Check if OLM is ready + if (!isReady || !olmAccount) { + return
Loading encryption keys...
+ } + + // Get the other user's id key and OT keys from the server to be prepared for messaging + if (!otherUser.olmAccount) { + return ( + { }}> + + +
+ +
+ Encryption Setup Required +
+ +
+

+ {otherUser.name} hasn't set up end-to-end encryption yet. +

+
+
+

+ β€’ + They need to log in and complete the encryption setup +

+

+ β€’ + Once complete, you'll be able to send encrypted messages +

+
+

+ πŸ”’ All messages are end-to-end encrypted for your privacy +

+
+
+
+ ) + } + + // Show error if session creation failed + if (sessionError) { + return ( +
+
+

Failed to create encryption session

+

{sessionError}

+
+
+ ); + } + + // Wait for session to be established + if (!olmSession) { + return ( +
+
+
+

Establishing secure connection...

+
+
+ ); + } + + return ( +
+
+
+
+ {/* Load more indicator */} + {hasMoreMessages && ( +
+ {isLoadingMore ? ( +
+
+ Loading older messages... +
+ ) : ( + + )} +
+ )} + {messages.map((msg, index) => { + const sender = participantDetails.find((p) => p.id === msg.fromUserId); + const selfDetail = participantDetails.find((p) => p.id === userId); + const isSelf = msg.fromUserId === userId; + const displayName = isSelf ? selfDetail?.displayUsername ?? selfDetail?.username ?? selfDetail?.name ?? "You" : (sender?.displayUsername ?? sender?.username ?? sender?.name ?? "Unknown"); + const timeLabel = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""; + + // Check if this message is from the same user as the previous one within 5 minutes + const prevMsg = index > 0 ? messages[index - 1] : null; + const isGrouped = prevMsg && + prevMsg.fromUserId === msg.fromUserId && + msg.timestamp && prevMsg.timestamp && + (msg.timestamp - prevMsg.timestamp) < 5 * 60 * 1000; + + return ( +
+ {!isGrouped ? ( + // Full message with avatar and header +
+ + + + {displayName.slice(0, 2).toUpperCase()} + + + +
+
+ + {displayName} + + + {timeLabel} + +
+
+ {msg.content} +
+
+
+ ) : ( + // Compact message without avatar (grouped) +
+
+ + {new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} + +
+
+ {msg.content} +
+
+ )} +
+ ); + })} + {/* Invisible element for auto-scrolling */} +
+
+
+
+ + {/* Message input */} +
+ setMessageInput(e.target.value)} + onKeyDown={async (e) => { + if (e.key === 'Enter' && !e.shiftKey && messageInput.trim() && password) { + e.preventDefault(); + try { + const messageId = await sendMessage({ + channelId, + content: messageInput, + fromUserId: userId, + to: otherUser.id, + timestamp: Date.now(), + status: "sent", + }, olmSession, sendMessageToServer, { + userId, + recipientId: otherUser.id, + password, + }); + if (messageId) { + setMessageInput(""); + } + } catch (error) { + console.error("[DMChannelContent] Failed to send message:", error); + toast.error("Failed to send message: " + (error instanceof Error ? error.message : "Unknown error")); + } + } + }} + /> +
+
+ ); +} \ No newline at end of file diff --git a/src/components/ui/friends/friend-list-item.tsx b/src/components/ui/friends/friend-list-item.tsx index 4b64618..a61a927 100644 --- a/src/components/ui/friends/friend-list-item.tsx +++ b/src/components/ui/friends/friend-list-item.tsx @@ -1,10 +1,10 @@ "use client" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { getOrCreateDmChannel } from "@/lib/db" import { MessageCircleIcon } from "lucide-react" import { useRouter } from "next/navigation" +import UserCard from "../user/user-card" import { FriendActionsMenu } from "./friend-actions-menu" export interface FriendData { @@ -45,12 +45,6 @@ export function FriendListItem({ const router = useRouter() const displayName = friend.displayUsername || friend.username || friend.name const status = friend.status?.status || "offline" - const statusColor = { - online: "bg-green-500", - idle: "bg-yellow-500", - dnd: "bg-red-500", - offline: "bg-gray-500" - }[status as "online" | "idle" | "dnd" | "offline"] return (
{/* Left side: Avatar + Info */}
-
- - - - {displayName?.charAt(0).toUpperCase()} - - -
-
+ +
{displayName} diff --git a/src/components/ui/layout/channel-list.tsx b/src/components/ui/layout/channel-list.tsx index a986620..aaf73cb 100644 --- a/src/components/ui/layout/channel-list.tsx +++ b/src/components/ui/layout/channel-list.tsx @@ -1,12 +1,12 @@ "use client" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" -import { db } from "@/lib/db" -import { cn } from "@/lib/utils" +import { clearUnread, db } from "@/lib/db" import { formatDistanceToNow } from "date-fns" +import { useLiveQuery } from "dexie-react-hooks" import { PlusIcon, SettingsIcon, UsersIcon, XIcon } from "lucide-react" import { useRouter } from "next/navigation" +import UserCard from "../user/user-card" export interface ChannelListProps { currentChannel: SiPher.Channel | null @@ -37,6 +37,11 @@ export function ChannelList({ }: ChannelListProps) { const router = useRouter() + const unreadCount = useLiveQuery( + () => db.unreadCounts.toArray(), + [] + ) + return (
{/* Channel List Header */} @@ -87,6 +92,7 @@ export function ChannelList({ const isActive = dmChannel?.id === channel.id const lastMessage = channel.times?.lastMessage const lastMessageTime = channel.times?.lastMessageAt + const channelUnreadCount = unreadCount?.find((unread) => unread.channelId === channel.id)?.count ?? 0 if (!channel.isOpen) return null; return ( @@ -96,22 +102,23 @@ export function ChannelList({ ? "bg-accent/60" : "hover:bg-accent/40" }`} - onClick={() => router.push(`/channels/me/${channel.id}`)} + onClick={() => { + clearUnread(channel.id) + console.log("Cleared unread count for channel", channel.id) + router.push(`/channels/me/${channel.id}`) + }} > - {/* Avatar */}
- - - - {channel.name?.charAt(0).toUpperCase()} - - - + {channelUnreadCount > 0 && ( + + {channelUnreadCount > 99 ? '99+' : channelUnreadCount} + + )}
{/* Channel Info */} diff --git a/src/components/ui/layout/main-content-layout.tsx b/src/components/ui/layout/main-content-layout.tsx index ee7d688..f61577d 100644 --- a/src/components/ui/layout/main-content-layout.tsx +++ b/src/components/ui/layout/main-content-layout.tsx @@ -7,7 +7,9 @@ import { useLiveQuery } from "dexie-react-hooks" import * as React from "react" import { useEffect, useMemo } from "react" import { api } from "../../../../convex/_generated/api" +import DMChannelContent from "../dm/DmChannelContent" import { FriendsPage } from "../friends/friends-page" +import { Spinner } from "../spinner" import { ChannelList } from "./channel-list" import { PageHeader } from "./page-header" import { SettingsPage } from "./settings-page" @@ -44,12 +46,13 @@ export function MainContentLayout({ [userId] ) ?? [] - const getParticipantDetails = useQuery(api.auth.getParticipantDetails, dmChannelId ? { - participantIds: openDmChannels - .find((channel) => channel.id === dmChannelId) - ?.participants - .filter((participant) => participant !== userId) ?? [] - } : "skip") + const participantIds = openDmChannels + .find((channel) => channel.id === dmChannelId) + ?.participants ?? [] + + const getParticipantDetails: SiPher.ParticipantDetail[] | undefined = useQuery(api.auth.getParticipantDetails, + { participantIds } + ) // Combine channel from local DB with participant details from Convex const dmChannel = useMemo(() => { @@ -60,7 +63,14 @@ export function MainContentLayout({ return { id: channel.id, - participantDetails: getParticipantDetails ?? [] + participantDetails: getParticipantDetails.map((participant) => ({ + id: participant.id as string, + name: participant.name, + username: participant.username ?? "", + displayUsername: participant.displayUsername ?? "", + image: participant.image ?? "", + status: participant.status, + })) } }, [openDmChannels, dmChannelId, getParticipantDetails]) @@ -104,12 +114,21 @@ export function MainContentLayout({ {/* Main Content */}
- {page === "dm" ? ( -
-

DM chat with {dmChannelId}

-
- ) : page === "server" ? ( -
+ {page === "dm" && dmChannelId ? ( + getParticipantDetails ? ( +
+ +
+ ) : ( +
+
+ +

Loading...

+
+
+ ) + ) : page === "server" && serverChannelId ? ( +

Server channel {serverChannelId}

) : page === "friends" ? ( @@ -132,5 +151,4 @@ export function MainContentLayout({ /> ) -} - +} \ No newline at end of file diff --git a/src/components/ui/layout/page-header.tsx b/src/components/ui/layout/page-header.tsx index c2a605c..db7073f 100644 --- a/src/components/ui/layout/page-header.tsx +++ b/src/components/ui/layout/page-header.tsx @@ -1,9 +1,8 @@ "use client" import { Button } from "@/components/ui/button" -import { cn } from "@/lib/utils" import { PhoneIcon, SearchIcon, UserIcon, UsersIcon, VideoIcon } from "lucide-react" -import { Avatar, AvatarFallback, AvatarImage } from "../avatar" +import UserCard from "../user/user-card" export interface PageHeaderProps { currentChannel: SiPher.Channel | null @@ -26,13 +25,6 @@ export interface PageHeaderProps { serverChannelId?: string } -const statusColors: Record<"online" | "busy" | "offline" | "away", string> = { - online: "bg-emerald-500", - busy: "bg-red-500", - away: "bg-yellow-500", - offline: "bg-muted-foreground" -}; - export function PageHeader({ currentChannel, page, @@ -63,20 +55,12 @@ export function PageHeader({ {/* Page title/options */} {dmChannel ? (
-
- - - - {dmChannel.participantDetails[0].name?.charAt(0).toUpperCase()} - - - -
+ {dmChannel.participantDetails[0].name}