feat: enhance user status management and introduce nests functionality
- Updated user status handling to include optional user-set status, improving user experience during reconnections. - Added new queries and mutations for managing nests, including fetching non-offline user IDs and forcing users offline. - Introduced new database schema for nests, roles, and channels, enhancing the application's organizational structure. - Updated dependencies in package.json and bun.lock for improved stability and compatibility. - Refactored related components and API to support the new nests functionality.
This commit is contained in:
parent
55e78db2cb
commit
e7dd6c961d
39 changed files with 3087 additions and 455 deletions
217
bun.lock
217
bun.lock
|
|
@ -5,60 +5,61 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "sipher",
|
"name": "sipher",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/better-auth": "latest",
|
"@convex-dev/better-auth": "^0.10.10",
|
||||||
"@marsidev/react-turnstile": "latest",
|
"@marsidev/react-turnstile": "^1.4.2",
|
||||||
"@matrix-org/olm": "latest",
|
"@matrix-org/olm": "^3.2.15",
|
||||||
"@nanostores/react": "latest",
|
"@nanostores/react": "^1.0.0",
|
||||||
"@phosphor-icons/react": "latest",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@radix-ui/react-avatar": "latest",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "latest",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-context-menu": "latest",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-dialog": "latest",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-hover-card": "latest",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-label": "latest",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-menubar": "latest",
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
"@radix-ui/react-popover": "latest",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "latest",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-scroll-area": "latest",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "latest",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "latest",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "latest",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/libsodium-wrappers": "latest",
|
"@types/libsodium-wrappers": "^0.7.14",
|
||||||
"better-auth": "latest",
|
"better-auth": "1.4.12",
|
||||||
"class-variance-authority": "latest",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "latest",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "latest",
|
"cmdk": "^1.1.1",
|
||||||
"convex": "latest",
|
"convex": "^1.31.7",
|
||||||
"cross-env": "latest",
|
"cross-env": "^10.1.0",
|
||||||
"date-fns": "latest",
|
"date-fns": "^4.1.0",
|
||||||
"dexie": "latest",
|
"dexie": "^4.3.0",
|
||||||
"dexie-react-hooks": "latest",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
"framer-motion": "latest",
|
"dotenv": "^17.3.1",
|
||||||
"lucide-react": "latest",
|
"framer-motion": "^12.34.0",
|
||||||
"moment": "latest",
|
"lucide-react": "^0.562.0",
|
||||||
"nanostores": "latest",
|
"moment": "^2.30.1",
|
||||||
"next": "latest",
|
"nanostores": "^1.1.0",
|
||||||
"next-themes": "latest",
|
"next": "16.1.1",
|
||||||
"react": "latest",
|
"next-themes": "^0.4.6",
|
||||||
"react-day-picker": "latest",
|
"react": "19.2.3",
|
||||||
"react-dom": "latest",
|
"react-day-picker": "^9.13.1",
|
||||||
"socket.io": "latest",
|
"react-dom": "19.2.3",
|
||||||
"socket.io-client": "latest",
|
"socket.io": "^4.8.3",
|
||||||
"sonner": "latest",
|
"socket.io-client": "^4.8.3",
|
||||||
"tailwind-merge": "latest",
|
"sonner": "^2.0.7",
|
||||||
"zod": "latest",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "latest",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "latest",
|
"@types/node": "^25.2.2",
|
||||||
"@types/react": "latest",
|
"@types/react": "^19.2.13",
|
||||||
"@types/react-dom": "latest",
|
"@types/react-dom": "^19.2.3",
|
||||||
"babel-plugin-react-compiler": "latest",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"tailwindcss": "latest",
|
"tailwindcss": "^4.1.18",
|
||||||
"tsx": "latest",
|
"tsx": "^4.21.0",
|
||||||
"tw-animate-css": "latest",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "latest",
|
"typescript": "^5.9.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -213,7 +214,7 @@
|
||||||
|
|
||||||
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
||||||
|
|
||||||
"@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=="],
|
"@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.4.2", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-xs1qOuyeMOz6t9BXXCXWiukC0/0+48vR08B7uwNdG05wCMnbcNgxiFmdFKDOFbM76qFYFRYlGeRfhfq1U/iZmA=="],
|
||||||
|
|
||||||
"@matrix-org/olm": ["@matrix-org/olm@3.2.15", "", {}, "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="],
|
"@matrix-org/olm": ["@matrix-org/olm@3.2.15", "", {}, "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q=="],
|
||||||
|
|
||||||
|
|
@ -359,45 +360,45 @@
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.2.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.0" } }, "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.0", "@tailwindcss/oxide-darwin-arm64": "4.2.0", "@tailwindcss/oxide-darwin-x64": "4.2.0", "@tailwindcss/oxide-freebsd-x64": "4.2.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", "@tailwindcss/oxide-linux-x64-musl": "4.2.0", "@tailwindcss/oxide-wasm32-wasi": "4.2.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" } }, "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.0", "", { "os": "android", "cpu": "arm64" }, "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.0", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ=="],
|
||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.18", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", "tailwindcss": "4.1.18" } }, "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g=="],
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.0", "@tailwindcss/oxide": "4.2.0", "postcss": "^8.5.6", "tailwindcss": "4.2.0" } }, "sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||||
|
|
||||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||||
|
|
||||||
"@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.7.14", "", {}, "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ=="],
|
"@types/libsodium-wrappers": ["@types/libsodium-wrappers@0.7.14", "", {}, "sha512-5Kv68fXuXK0iDuUir1WPGw2R9fOZUlYlSAa0ztMcL0s0BfIDTqg9GXz8K30VJpPP3sxWhbolnQma2x+/TfkzDQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="],
|
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
|
|
@ -417,7 +418,7 @@
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="],
|
||||||
|
|
||||||
|
|
@ -431,7 +432,7 @@
|
||||||
|
|
||||||
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
|
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
|
||||||
|
|
||||||
"convex": ["convex@1.31.4", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-iDm283Gb/CFRb30cvhH6Z9qlYof6dhtin415FarKUKB3K7gumO0rn8snY0CTvUrThV3UnCtttbuL/1oY7LscyA=="],
|
"convex": ["convex@1.32.0", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-5FlajdLpW75pdLS+/CgGH5H6yeRuA+ru50AKJEYbJpmyILUS+7fdTvsdTaQ7ZFXMv0gE8mX4S+S3AtJ94k0mfw=="],
|
||||||
|
|
||||||
"convex-helpers": ["convex-helpers@0.1.106", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.25.4", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-hWRe3yDaAVHMe4CUYw1YoQLiPZ1KIx6Kbf0w6UcRDx1BXpJgMCl3GVIMiSeYiA0PkbwjnIwGWIvoUVKloG5Tyw=="],
|
"convex-helpers": ["convex-helpers@0.1.106", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.25.4", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-hWRe3yDaAVHMe4CUYw1YoQLiPZ1KIx6Kbf0w6UcRDx1BXpJgMCl3GVIMiSeYiA0PkbwjnIwGWIvoUVKloG5Tyw=="],
|
||||||
|
|
||||||
|
|
@ -457,21 +458,23 @@
|
||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
"dexie": ["dexie@4.2.1", "", {}, "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="],
|
"dexie": ["dexie@4.3.0", "", {}, "sha512-5EeoQpJvMKHe6zWt/FSIIuRa3CWlZeIl6zKXt+Lz7BU6RoRRLgX9dZEynRfXrkLcldKYCBiz7xekTEylnie1Ug=="],
|
||||||
|
|
||||||
"dexie-react-hooks": ["dexie-react-hooks@4.2.0", "", { "peerDependencies": { "@types/react": ">=16", "dexie": ">=4.2.0-alpha.1 <5.0.0", "react": ">=16" } }, "sha512-u7KqTX9JpBQK8+tEyA9X0yMGXlSCsbm5AU64N6gjvGk/IutYDpLBInMYEAEC83s3qhIvryFS+W+sqLZUBEvePQ=="],
|
"dexie-react-hooks": ["dexie-react-hooks@4.2.0", "", { "peerDependencies": { "@types/react": ">=16", "dexie": ">=4.2.0-alpha.1 <5.0.0", "react": ">=16" } }, "sha512-u7KqTX9JpBQK8+tEyA9X0yMGXlSCsbm5AU64N6gjvGk/IutYDpLBInMYEAEC83s3qhIvryFS+W+sqLZUBEvePQ=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||||
|
|
||||||
"engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="],
|
"engine.io": ["engine.io@6.6.4", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g=="],
|
||||||
|
|
||||||
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
|
"engine.io-client": ["engine.io-client@6.6.3", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.1.1" } }, "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w=="],
|
||||||
|
|
||||||
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
|
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||||
|
|
||||||
"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=="],
|
"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.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "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-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
|
"framer-motion": ["framer-motion@12.34.2", "", { "dependencies": { "motion-dom": "^12.34.2", "motion-utils": "^12.29.2", "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-CcnYTzbRybm1/OE8QLXfXI8gR1cx5T4dF3D2kn5IyqsGNeLAKl2iFHb2BzFyXBGqESntDt6rPYl4Jhrb7tdB8g=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
|
@ -489,29 +492,29 @@
|
||||||
|
|
||||||
"kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="],
|
"kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="],
|
||||||
|
|
||||||
"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=="],
|
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||||
|
|
||||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||||
|
|
||||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
||||||
|
|
||||||
|
|
@ -523,9 +526,9 @@
|
||||||
|
|
||||||
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.26.2", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw=="],
|
"motion-dom": ["motion-dom@12.34.2", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-n7gknp7gHcW7DUcmet0JVPLVHmE3j9uWwDp5VbE3IkCNnW5qdu0mOhjNYzXMkrQjrgr+h6Db3EDM2QBhW2qNxQ=="],
|
||||||
|
|
||||||
"motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="],
|
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
|
@ -555,7 +558,7 @@
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
||||||
"react-day-picker": ["react-day-picker@9.13.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ=="],
|
"react-day-picker": ["react-day-picker@9.13.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||||
|
|
||||||
|
|
@ -599,9 +602,9 @@
|
||||||
|
|
||||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
"tailwindcss": ["tailwindcss@4.2.0", "", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
|
@ -617,7 +620,7 @@
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
|
|
||||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
|
|
@ -629,16 +632,20 @@
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||||
|
|
||||||
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
"xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"@better-auth/core/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/@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=="],
|
"@better-auth/passkey/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="],
|
||||||
|
|
||||||
|
"@convex-dev/better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
"@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
@ -707,13 +714,13 @@
|
||||||
|
|
||||||
"@radix-ui/react-visually-hidden/@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=="],
|
"@radix-ui/react-visually-hidden/@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=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.0", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
|
@ -721,18 +728,26 @@
|
||||||
|
|
||||||
"@types/cors/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
"@types/cors/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||||
|
|
||||||
|
"better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||||
|
|
||||||
"convex/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
|
"convex/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
|
||||||
|
|
||||||
"engine.io/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
"engine.io/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||||
|
|
||||||
"engine.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
"engine.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
|
"engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
"engine.io-client/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
|
"engine.io-client/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
"socket.io-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
"socket.io-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
|
"socket.io-adapter/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="],
|
||||||
|
|
||||||
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||||
|
|
||||||
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
"tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||||
|
|
@ -761,6 +776,8 @@
|
||||||
|
|
||||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@types/cors/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
||||||
|
|
||||||
"convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
|
"convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
|
||||||
|
|
@ -812,5 +829,7 @@
|
||||||
"convex/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
|
"convex/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
|
||||||
|
|
||||||
"convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
|
"convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
|
||||||
|
|
||||||
|
"engine.io/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
719
convex/_generated/api.d.ts
vendored
719
convex/_generated/api.d.ts
vendored
|
|
@ -67,6 +67,7 @@ export declare const components: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
|
nests?: Array<string>;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -75,10 +76,14 @@ export declare const components: {
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
isUserSet: boolean;
|
|
||||||
status: "online" | "busy" | "offline" | "away";
|
status: "online" | "busy" | "offline" | "away";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
userSetStatus?: {
|
||||||
|
isSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
model: "userStatus";
|
model: "userStatus";
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +105,63 @@ export declare const components: {
|
||||||
data: { createdAt: number; friendId: string; userId: string };
|
data: { createdAt: number; friendId: string; userId: string };
|
||||||
model: "friends";
|
model: "friends";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
channels: Array<string>;
|
||||||
|
colors?: { accent: string; primary: string };
|
||||||
|
createdAt: number;
|
||||||
|
description?: string;
|
||||||
|
emojis: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
images?: { banner: string; icon: string };
|
||||||
|
managerId: string;
|
||||||
|
members: Array<string>;
|
||||||
|
name: string;
|
||||||
|
onDiscover?: boolean;
|
||||||
|
region?: string;
|
||||||
|
roles: Array<string>;
|
||||||
|
type: "global" | "regional" | "private";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
model: "nests";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
color?: string;
|
||||||
|
createdAt: number;
|
||||||
|
flags: Array<bigint>;
|
||||||
|
hoist?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
members: Array<string>;
|
||||||
|
mentionable?: boolean;
|
||||||
|
name: string;
|
||||||
|
nestId: string;
|
||||||
|
permissions: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
model: "roles";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
createdAt: number;
|
||||||
|
name: string;
|
||||||
|
nestId: string;
|
||||||
|
overwrites: Array<{
|
||||||
|
allow: Array<bigint> | null;
|
||||||
|
deny: Array<bigint> | null;
|
||||||
|
id: string | string;
|
||||||
|
}>;
|
||||||
|
permissions: Array<bigint>;
|
||||||
|
position: number;
|
||||||
|
type: "text" | "category" | "announcement";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
model: "channels";
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
attachments?: Array<string>;
|
attachments?: Array<string>;
|
||||||
|
|
@ -182,8 +244,11 @@ export declare const components: {
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
|
createdAt?: number;
|
||||||
identityKey: { curve25519: string; ed25519: string };
|
identityKey: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
updatedAt?: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
|
|
@ -213,6 +278,7 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -242,7 +308,7 @@ export declare const components: {
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -328,6 +394,121 @@ export declare const components: {
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
|
|
@ -540,7 +721,14 @@ export declare const components: {
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -594,6 +782,7 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -623,7 +812,7 @@ export declare const components: {
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -709,6 +898,121 @@ export declare const components: {
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
|
|
@ -921,7 +1225,14 @@ export declare const components: {
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -958,6 +1269,9 @@ export declare const components: {
|
||||||
| "userStatus"
|
| "userStatus"
|
||||||
| "friendRequests"
|
| "friendRequests"
|
||||||
| "friends"
|
| "friends"
|
||||||
|
| "nests"
|
||||||
|
| "roles"
|
||||||
|
| "channels"
|
||||||
| "messages"
|
| "messages"
|
||||||
| "attachments"
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
|
|
@ -1011,6 +1325,9 @@ export declare const components: {
|
||||||
| "userStatus"
|
| "userStatus"
|
||||||
| "friendRequests"
|
| "friendRequests"
|
||||||
| "friends"
|
| "friends"
|
||||||
|
| "nests"
|
||||||
|
| "roles"
|
||||||
|
| "channels"
|
||||||
| "messages"
|
| "messages"
|
||||||
| "attachments"
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
|
|
@ -1062,6 +1379,7 @@ export declare const components: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
};
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
nests?: Array<string>;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -1079,6 +1397,7 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1104,17 +1423,21 @@ export declare const components: {
|
||||||
| {
|
| {
|
||||||
model: "userStatus";
|
model: "userStatus";
|
||||||
update: {
|
update: {
|
||||||
isUserSet?: boolean;
|
|
||||||
status?: "online" | "busy" | "offline" | "away";
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
userSetStatus?: {
|
||||||
|
isSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -1216,6 +1539,169 @@ export declare const components: {
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
update: {
|
||||||
|
channels?: Array<string>;
|
||||||
|
colors?: { accent: string; primary: string };
|
||||||
|
createdAt?: number;
|
||||||
|
description?: string;
|
||||||
|
emojis?: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
images?: { banner: string; icon: string };
|
||||||
|
managerId?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
name?: string;
|
||||||
|
onDiscover?: boolean;
|
||||||
|
region?: string;
|
||||||
|
roles?: Array<string>;
|
||||||
|
type?: "global" | "regional" | "private";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
update: {
|
||||||
|
color?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
flags?: Array<bigint>;
|
||||||
|
hoist?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
mentionable?: boolean;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
overwrites?: Array<{
|
||||||
|
allow: Array<bigint> | null;
|
||||||
|
deny: Array<bigint> | null;
|
||||||
|
id: string | string;
|
||||||
|
}>;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
type?: "text" | "category" | "announcement";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
update: {
|
update: {
|
||||||
|
|
@ -1489,13 +1975,23 @@ export declare const components: {
|
||||||
| {
|
| {
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
update: {
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
identityKey?: { curve25519: string; ed25519: string };
|
identityKey?: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -1546,6 +2042,7 @@ export declare const components: {
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
};
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
nests?: Array<string>;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -1563,6 +2060,7 @@ export declare const components: {
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1588,17 +2086,21 @@ export declare const components: {
|
||||||
| {
|
| {
|
||||||
model: "userStatus";
|
model: "userStatus";
|
||||||
update: {
|
update: {
|
||||||
isUserSet?: boolean;
|
|
||||||
status?: "online" | "busy" | "offline" | "away";
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
userSetStatus?: {
|
||||||
|
isSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -1700,6 +2202,169 @@ export declare const components: {
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
update: {
|
||||||
|
channels?: Array<string>;
|
||||||
|
colors?: { accent: string; primary: string };
|
||||||
|
createdAt?: number;
|
||||||
|
description?: string;
|
||||||
|
emojis?: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
images?: { banner: string; icon: string };
|
||||||
|
managerId?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
name?: string;
|
||||||
|
onDiscover?: boolean;
|
||||||
|
region?: string;
|
||||||
|
roles?: Array<string>;
|
||||||
|
type?: "global" | "regional" | "private";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
update: {
|
||||||
|
color?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
flags?: Array<bigint>;
|
||||||
|
hoist?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
mentionable?: boolean;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
overwrites?: Array<{
|
||||||
|
allow: Array<bigint> | null;
|
||||||
|
deny: Array<bigint> | null;
|
||||||
|
id: string | string;
|
||||||
|
}>;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
type?: "text" | "category" | "announcement";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
update: {
|
update: {
|
||||||
|
|
@ -1973,13 +2638,23 @@ export declare const components: {
|
||||||
| {
|
| {
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
update: {
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
identityKey?: { curve25519: string; ed25519: string };
|
identityKey?: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -2006,6 +2681,12 @@ export declare const components: {
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
nests: {
|
||||||
|
locals: {
|
||||||
|
getRecommendedNests: FunctionReference<"query", "internal", any, any>;
|
||||||
|
getUserNests: FunctionReference<"query", "internal", any, any>;
|
||||||
|
};
|
||||||
|
};
|
||||||
olm: {
|
olm: {
|
||||||
index: {
|
index: {
|
||||||
consumeOTK: FunctionReference<
|
consumeOTK: FunctionReference<
|
||||||
|
|
@ -2014,6 +2695,13 @@ export declare const components: {
|
||||||
{ keyId: string; userId: string },
|
{ keyId: string; userId: string },
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
|
getKeyVersion: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ userId: string },
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
migrateOlmAccounts: FunctionReference<"mutation", "internal", any, any>;
|
||||||
retrieveServerOlmAccount: FunctionReference<
|
retrieveServerOlmAccount: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
|
|
@ -2041,8 +2729,15 @@ export declare const components: {
|
||||||
{ answer: "accept" | "decline" | "ignore"; requestId: string },
|
{ answer: "accept" | "decline" | "ignore"; requestId: string },
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
|
forceUserOffline: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ userId: string },
|
||||||
|
any
|
||||||
|
>;
|
||||||
getFriendRequests: FunctionReference<"query", "internal", any, any>;
|
getFriendRequests: FunctionReference<"query", "internal", any, any>;
|
||||||
getFriends: FunctionReference<"query", "internal", any, any>;
|
getFriends: FunctionReference<"query", "internal", any, any>;
|
||||||
|
getNonOfflineUserIds: FunctionReference<"query", "internal", {}, any>;
|
||||||
getParticipantDetails: FunctionReference<
|
getParticipantDetails: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
|
|
@ -2066,7 +2761,7 @@ export declare const components: {
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{
|
{
|
||||||
isUserSet: boolean;
|
isUserSet?: boolean;
|
||||||
status: "online" | "busy" | "offline" | "away";
|
status: "online" | "busy" | "offline" | "away";
|
||||||
},
|
},
|
||||||
any
|
any
|
||||||
|
|
|
||||||
2
convex/_generated/dataModel.d.ts
vendored
2
convex/_generated/dataModel.d.ts
vendored
|
|
@ -38,7 +38,7 @@ export type Doc = any;
|
||||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||||
*
|
*
|
||||||
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||||
*
|
*
|
||||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
* strings when type checking.
|
* strings when type checking.
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ export const retrieveServerOlmAccount = query({
|
||||||
export const updateUserStatus = mutation({
|
export const updateUserStatus = mutation({
|
||||||
args: {
|
args: {
|
||||||
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
isUserSet: v.boolean(),
|
isUserSet: v.optional(v.boolean()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
return ctx.runMutation(components.betterAuth.user.index.updateUserStatus, {
|
return ctx.runMutation(components.betterAuth.user.index.updateUserStatus, {
|
||||||
|
|
@ -132,6 +132,24 @@ export const updateUserStatus = mutation({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getNonOfflineUserIds = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return ctx.runQuery(components.betterAuth.user.index.getNonOfflineUserIds, {});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const forceUserOffline = mutation({
|
||||||
|
args: {
|
||||||
|
userId: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.runMutation(components.betterAuth.user.index.forceUserOffline, {
|
||||||
|
userId: args.userId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const updateUserMetadata = mutation({
|
export const updateUserMetadata = mutation({
|
||||||
args: {
|
args: {
|
||||||
metadata: v.object({
|
metadata: v.object({
|
||||||
|
|
@ -212,4 +230,27 @@ export const consumeOTK = mutation({
|
||||||
keyId: args.keyId,
|
keyId: args.keyId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getKeyVersion = query({
|
||||||
|
args: {
|
||||||
|
userId: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.runQuery(components.betterAuth.olm.index.getKeyVersion, {
|
||||||
|
userId: args.userId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const migrateOlmAccounts = mutation({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return ctx.runMutation(components.betterAuth.olm.index.migrateOlmAccounts, {});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUserNests = query({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
return ctx.runQuery(components.betterAuth.nests.locals.getUserNests);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -10,7 +10,9 @@
|
||||||
|
|
||||||
import type * as adapter from "../adapter.js";
|
import type * as adapter from "../adapter.js";
|
||||||
import type * as auth from "../auth.js";
|
import type * as auth from "../auth.js";
|
||||||
|
import type * as nests_locals from "../nests/locals.js";
|
||||||
import type * as olm_index from "../olm/index.js";
|
import type * as olm_index from "../olm/index.js";
|
||||||
|
import type * as schemas_nests from "../schemas/nests.js";
|
||||||
import type * as schemas_user from "../schemas/user.js";
|
import type * as schemas_user from "../schemas/user.js";
|
||||||
import type * as user_index from "../user/index.js";
|
import type * as user_index from "../user/index.js";
|
||||||
|
|
||||||
|
|
@ -24,7 +26,9 @@ import { anyApi, componentsGeneric } from "convex/server";
|
||||||
const fullApi: ApiFromModules<{
|
const fullApi: ApiFromModules<{
|
||||||
adapter: typeof adapter;
|
adapter: typeof adapter;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
|
"nests/locals": typeof nests_locals;
|
||||||
"olm/index": typeof olm_index;
|
"olm/index": typeof olm_index;
|
||||||
|
"schemas/nests": typeof schemas_nests;
|
||||||
"schemas/user": typeof schemas_user;
|
"schemas/user": typeof schemas_user;
|
||||||
"user/index": typeof user_index;
|
"user/index": typeof user_index;
|
||||||
}> = anyApi as any;
|
}> = anyApi as any;
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
|
nests?: Array<string>;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -48,10 +49,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
isUserSet: boolean;
|
|
||||||
status: "online" | "busy" | "offline" | "away";
|
status: "online" | "busy" | "offline" | "away";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
userSetStatus?: {
|
||||||
|
isSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
model: "userStatus";
|
model: "userStatus";
|
||||||
}
|
}
|
||||||
|
|
@ -73,6 +78,63 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
data: { createdAt: number; friendId: string; userId: string };
|
data: { createdAt: number; friendId: string; userId: string };
|
||||||
model: "friends";
|
model: "friends";
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
channels: Array<string>;
|
||||||
|
colors?: { accent: string; primary: string };
|
||||||
|
createdAt: number;
|
||||||
|
description?: string;
|
||||||
|
emojis: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
images?: { banner: string; icon: string };
|
||||||
|
managerId: string;
|
||||||
|
members: Array<string>;
|
||||||
|
name: string;
|
||||||
|
onDiscover?: boolean;
|
||||||
|
region?: string;
|
||||||
|
roles: Array<string>;
|
||||||
|
type: "global" | "regional" | "private";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
model: "nests";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
color?: string;
|
||||||
|
createdAt: number;
|
||||||
|
flags: Array<bigint>;
|
||||||
|
hoist?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
members: Array<string>;
|
||||||
|
mentionable?: boolean;
|
||||||
|
name: string;
|
||||||
|
nestId: string;
|
||||||
|
permissions: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
model: "roles";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
data: {
|
||||||
|
createdAt: number;
|
||||||
|
name: string;
|
||||||
|
nestId: string;
|
||||||
|
overwrites: Array<{
|
||||||
|
allow: Array<bigint> | null;
|
||||||
|
deny: Array<bigint> | null;
|
||||||
|
id: string | string;
|
||||||
|
}>;
|
||||||
|
permissions: Array<bigint>;
|
||||||
|
position: number;
|
||||||
|
type: "text" | "category" | "announcement";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
|
model: "channels";
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
attachments?: Array<string>;
|
attachments?: Array<string>;
|
||||||
|
|
@ -155,8 +217,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: {
|
data: {
|
||||||
|
createdAt?: number;
|
||||||
identityKey: { curve25519: string; ed25519: string };
|
identityKey: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
updatedAt?: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
|
|
@ -187,6 +252,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -216,7 +282,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -302,6 +368,121 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
|
|
@ -514,7 +695,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -569,6 +757,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -598,7 +787,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -684,6 +873,121 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
|
|
@ -896,7 +1200,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -934,6 +1245,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "userStatus"
|
| "userStatus"
|
||||||
| "friendRequests"
|
| "friendRequests"
|
||||||
| "friends"
|
| "friends"
|
||||||
|
| "nests"
|
||||||
|
| "roles"
|
||||||
|
| "channels"
|
||||||
| "messages"
|
| "messages"
|
||||||
| "attachments"
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
|
|
@ -988,6 +1302,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "userStatus"
|
| "userStatus"
|
||||||
| "friendRequests"
|
| "friendRequests"
|
||||||
| "friends"
|
| "friends"
|
||||||
|
| "nests"
|
||||||
|
| "roles"
|
||||||
|
| "channels"
|
||||||
| "messages"
|
| "messages"
|
||||||
| "attachments"
|
| "attachments"
|
||||||
| "session"
|
| "session"
|
||||||
|
|
@ -1040,6 +1357,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
};
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
nests?: Array<string>;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -1057,6 +1375,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1082,17 +1401,21 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| {
|
| {
|
||||||
model: "userStatus";
|
model: "userStatus";
|
||||||
update: {
|
update: {
|
||||||
isUserSet?: boolean;
|
|
||||||
status?: "online" | "busy" | "offline" | "away";
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
userSetStatus?: {
|
||||||
|
isSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -1194,6 +1517,169 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
update: {
|
||||||
|
channels?: Array<string>;
|
||||||
|
colors?: { accent: string; primary: string };
|
||||||
|
createdAt?: number;
|
||||||
|
description?: string;
|
||||||
|
emojis?: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
images?: { banner: string; icon: string };
|
||||||
|
managerId?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
name?: string;
|
||||||
|
onDiscover?: boolean;
|
||||||
|
region?: string;
|
||||||
|
roles?: Array<string>;
|
||||||
|
type?: "global" | "regional" | "private";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
update: {
|
||||||
|
color?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
flags?: Array<bigint>;
|
||||||
|
hoist?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
mentionable?: boolean;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
overwrites?: Array<{
|
||||||
|
allow: Array<bigint> | null;
|
||||||
|
deny: Array<bigint> | null;
|
||||||
|
id: string | string;
|
||||||
|
}>;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
type?: "text" | "category" | "announcement";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
update: {
|
update: {
|
||||||
|
|
@ -1467,13 +1953,23 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| {
|
| {
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
update: {
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
identityKey?: { curve25519: string; ed25519: string };
|
identityKey?: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -1525,6 +2021,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
phrasePreference: "comforting" | "mocking" | "both";
|
phrasePreference: "comforting" | "mocking" | "both";
|
||||||
};
|
};
|
||||||
name?: string;
|
name?: string;
|
||||||
|
nests?: Array<string>;
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: null | string;
|
userId?: null | string;
|
||||||
username?: null | string;
|
username?: null | string;
|
||||||
|
|
@ -1542,6 +2039,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| "username"
|
| "username"
|
||||||
| "displayUsername"
|
| "displayUsername"
|
||||||
| "metadata"
|
| "metadata"
|
||||||
|
| "nests"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
|
|
@ -1567,17 +2065,21 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| {
|
| {
|
||||||
model: "userStatus";
|
model: "userStatus";
|
||||||
update: {
|
update: {
|
||||||
isUserSet?: boolean;
|
|
||||||
status?: "online" | "busy" | "offline" | "away";
|
status?: "online" | "busy" | "offline" | "away";
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
userSetStatus?: {
|
||||||
|
isSet: boolean;
|
||||||
|
status: "online" | "busy" | "offline" | "away";
|
||||||
|
updatedAt: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field:
|
field:
|
||||||
| "userId"
|
| "userId"
|
||||||
| "status"
|
| "status"
|
||||||
| "isUserSet"
|
| "userSetStatus"
|
||||||
| "updatedAt"
|
| "updatedAt"
|
||||||
| "_id";
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
|
|
@ -1679,6 +2181,169 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| null;
|
| null;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
model: "nests";
|
||||||
|
update: {
|
||||||
|
channels?: Array<string>;
|
||||||
|
colors?: { accent: string; primary: string };
|
||||||
|
createdAt?: number;
|
||||||
|
description?: string;
|
||||||
|
emojis?: Array<{
|
||||||
|
createdAt: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
images?: { banner: string; icon: string };
|
||||||
|
managerId?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
name?: string;
|
||||||
|
onDiscover?: boolean;
|
||||||
|
region?: string;
|
||||||
|
roles?: Array<string>;
|
||||||
|
type?: "global" | "regional" | "private";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "description"
|
||||||
|
| "images"
|
||||||
|
| "colors"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "managerId"
|
||||||
|
| "members"
|
||||||
|
| "channels"
|
||||||
|
| "roles"
|
||||||
|
| "region"
|
||||||
|
| "emojis"
|
||||||
|
| "onDiscover"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "roles";
|
||||||
|
update: {
|
||||||
|
color?: string;
|
||||||
|
createdAt?: number;
|
||||||
|
flags?: Array<bigint>;
|
||||||
|
hoist?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
members?: Array<string>;
|
||||||
|
mentionable?: boolean;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "nestId"
|
||||||
|
| "name"
|
||||||
|
| "color"
|
||||||
|
| "hoist"
|
||||||
|
| "mentionable"
|
||||||
|
| "icon"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "flags"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "members"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
model: "channels";
|
||||||
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
|
name?: string;
|
||||||
|
nestId?: string;
|
||||||
|
overwrites?: Array<{
|
||||||
|
allow: Array<bigint> | null;
|
||||||
|
deny: Array<bigint> | null;
|
||||||
|
id: string | string;
|
||||||
|
}>;
|
||||||
|
permissions?: Array<bigint>;
|
||||||
|
position?: number;
|
||||||
|
type?: "text" | "category" | "announcement";
|
||||||
|
updatedAt?: number;
|
||||||
|
};
|
||||||
|
where?: Array<{
|
||||||
|
connector?: "AND" | "OR";
|
||||||
|
field:
|
||||||
|
| "type"
|
||||||
|
| "name"
|
||||||
|
| "nestId"
|
||||||
|
| "position"
|
||||||
|
| "permissions"
|
||||||
|
| "overwrites"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "_id";
|
||||||
|
operator?:
|
||||||
|
| "lt"
|
||||||
|
| "lte"
|
||||||
|
| "gt"
|
||||||
|
| "gte"
|
||||||
|
| "eq"
|
||||||
|
| "in"
|
||||||
|
| "not_in"
|
||||||
|
| "ne"
|
||||||
|
| "contains"
|
||||||
|
| "starts_with"
|
||||||
|
| "ends_with";
|
||||||
|
value:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Array<string>
|
||||||
|
| Array<number>
|
||||||
|
| null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
model: "messages";
|
model: "messages";
|
||||||
update: {
|
update: {
|
||||||
|
|
@ -1952,13 +2617,23 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
| {
|
| {
|
||||||
model: "olmAccount";
|
model: "olmAccount";
|
||||||
update: {
|
update: {
|
||||||
|
createdAt?: number;
|
||||||
identityKey?: { curve25519: string; ed25519: string };
|
identityKey?: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys?: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
updatedAt?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
};
|
};
|
||||||
where?: Array<{
|
where?: Array<{
|
||||||
connector?: "AND" | "OR";
|
connector?: "AND" | "OR";
|
||||||
field: "userId" | "identityKey" | "oneTimeKeys" | "_id";
|
field:
|
||||||
|
| "userId"
|
||||||
|
| "identityKey"
|
||||||
|
| "oneTimeKeys"
|
||||||
|
| "createdAt"
|
||||||
|
| "updatedAt"
|
||||||
|
| "keyVersion"
|
||||||
|
| "_id";
|
||||||
operator?:
|
operator?:
|
||||||
| "lt"
|
| "lt"
|
||||||
| "lte"
|
| "lte"
|
||||||
|
|
@ -1986,6 +2661,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
Name
|
Name
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
nests: {
|
||||||
|
locals: {
|
||||||
|
getRecommendedNests: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
any,
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
getUserNests: FunctionReference<"query", "internal", any, any, Name>;
|
||||||
|
};
|
||||||
|
};
|
||||||
olm: {
|
olm: {
|
||||||
index: {
|
index: {
|
||||||
consumeOTK: FunctionReference<
|
consumeOTK: FunctionReference<
|
||||||
|
|
@ -1995,6 +2682,20 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
any,
|
any,
|
||||||
Name
|
Name
|
||||||
>;
|
>;
|
||||||
|
getKeyVersion: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ userId: string },
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
|
migrateOlmAccounts: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
any,
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
retrieveServerOlmAccount: FunctionReference<
|
retrieveServerOlmAccount: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
|
|
@ -2025,6 +2726,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
any,
|
any,
|
||||||
Name
|
Name
|
||||||
>;
|
>;
|
||||||
|
forceUserOffline: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ userId: string },
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
getFriendRequests: FunctionReference<
|
getFriendRequests: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
|
|
@ -2033,6 +2741,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
Name
|
Name
|
||||||
>;
|
>;
|
||||||
getFriends: FunctionReference<"query", "internal", any, any, Name>;
|
getFriends: FunctionReference<"query", "internal", any, any, Name>;
|
||||||
|
getNonOfflineUserIds: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{},
|
||||||
|
any,
|
||||||
|
Name
|
||||||
|
>;
|
||||||
getParticipantDetails: FunctionReference<
|
getParticipantDetails: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
|
|
@ -2059,7 +2774,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{
|
{
|
||||||
isUserSet: boolean;
|
isUserSet?: boolean;
|
||||||
status: "online" | "busy" | "offline" | "away";
|
status: "online" | "busy" | "offline" | "away";
|
||||||
},
|
},
|
||||||
any,
|
any,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
|
||||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||||
*
|
*
|
||||||
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||||
*
|
*
|
||||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||||
* strings when type checking.
|
* strings when type checking.
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,6 @@ export const internalAction: ActionBuilder<DataModel, "internal"> =
|
||||||
*/
|
*/
|
||||||
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
||||||
|
|
||||||
type GenericCtx =
|
|
||||||
| GenericActionCtx<DataModel>
|
|
||||||
| GenericMutationCtx<DataModel>
|
|
||||||
| GenericQueryCtx<DataModel>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of services for use within Convex query functions.
|
* A set of services for use within Convex query functions.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
60
convex/betterAuth/nests/locals.ts
Normal file
60
convex/betterAuth/nests/locals.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { UserIdentity } from "convex/server";
|
||||||
|
import { Doc, Id } from "../_generated/dataModel";
|
||||||
|
import { MutationCtx, query, QueryCtx } from "../_generated/server";
|
||||||
|
|
||||||
|
// Overload signatures
|
||||||
|
async function userValidation(ctx: MutationCtx | QueryCtx, options: { required: false }): Promise<{ userId: Id<"user">; user: any } | null>;
|
||||||
|
async function userValidation(ctx: MutationCtx | QueryCtx, options?: { required?: true }): Promise<{ userId: Id<"user">; user: UserIdentity }>;
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
async function userValidation(ctx: MutationCtx | QueryCtx, options?: { required?: boolean }) {
|
||||||
|
const required = options?.required ?? true;
|
||||||
|
|
||||||
|
const user = await ctx.auth.getUserIdentity();
|
||||||
|
if (!user) {
|
||||||
|
if (required) throw new Error("User not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = ctx.db.normalizeId("user", user.subject as string) as Id<"user">;
|
||||||
|
if (!userId) {
|
||||||
|
if (required) throw new Error("User not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { userId, user };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserNests = query({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const { userId } = await userValidation(ctx, { required: true });
|
||||||
|
if (!userId) throw new Error("User not found");
|
||||||
|
|
||||||
|
const getUser = await ctx.db.get<"user">(userId);
|
||||||
|
if (!getUser) throw new Error("User not found");
|
||||||
|
else if (!getUser.nests || getUser.nests.length === 0) return [];
|
||||||
|
|
||||||
|
// Get the nests the user is a member of
|
||||||
|
const nests: Doc<"nests">[] = [];
|
||||||
|
for (const nestId of getUser.nests) {
|
||||||
|
const nest = await ctx.db.get<"nests">(nestId);
|
||||||
|
if (!nest) continue;
|
||||||
|
nests.push(nest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nests;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getRecommendedNests = query({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const { userId } = await userValidation(ctx, { required: true });
|
||||||
|
if (!userId) throw new Error("User not found");
|
||||||
|
|
||||||
|
const getUser = await ctx.db.get<"user">(userId);
|
||||||
|
if (!getUser) throw new Error("User not found");
|
||||||
|
|
||||||
|
const nests = await ctx.db.query<"nests">("nests").withIndex("onDiscover", q => q.eq("onDiscover", true)).collect();
|
||||||
|
return nests;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { Id } from "../../_generated/dataModel";
|
|
||||||
import { mutation, query } from "../_generated/server";
|
import { mutation, query } from "../_generated/server";
|
||||||
|
|
||||||
export const sendKeysToServer = mutation({
|
export const sendKeysToServer = mutation({
|
||||||
|
|
@ -16,22 +15,40 @@ export const sendKeysToServer = mutation({
|
||||||
forceInsert: v.boolean(), // if true, insert even if user already has an olm account
|
forceInsert: v.boolean(), // if true, insert even if user already has an olm account
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
// check if user already has an olm account
|
// check if user already has an olm account
|
||||||
const olmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", args.userId)).first();
|
const olmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", args.userId)).first();
|
||||||
|
|
||||||
if (olmAccount && !args.forceInsert) {
|
if (olmAccount && !args.forceInsert) {
|
||||||
throw new Error("User already has an olm account");
|
throw new Error("User already has an olm account");
|
||||||
|
} else if (olmAccount && args.forceInsert) {
|
||||||
|
// Keys are being rotated - increment version and update timestamp
|
||||||
|
await ctx.db.patch(olmAccount._id, {
|
||||||
|
identityKey: args.identityKey,
|
||||||
|
oneTimeKeys: args.oneTimeKeys,
|
||||||
|
updatedAt: now,
|
||||||
|
keyVersion: (olmAccount.keyVersion || 0) + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify all users who have sessions with this user that their sessions are now invalid
|
||||||
|
// This will be handled client-side by checking key versions
|
||||||
|
console.log(`[OLM] Keys rotated for user ${args.userId}, new version: ${(olmAccount.keyVersion || 0) + 1}`);
|
||||||
|
|
||||||
|
return { ...olmAccount, keyVersion: (olmAccount.keyVersion || 0) + 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const insert = await ctx.db.insert<"olmAccount">("olmAccount", {
|
// Create new account with initial key version
|
||||||
|
const newOlmAccount = await ctx.db.insert<"olmAccount">("olmAccount", {
|
||||||
userId: args.userId,
|
userId: args.userId,
|
||||||
identityKey: args.identityKey,
|
identityKey: args.identityKey,
|
||||||
oneTimeKeys: args.oneTimeKeys,
|
oneTimeKeys: args.oneTimeKeys || [],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
keyVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("insert", insert);
|
return newOlmAccount;
|
||||||
return insert;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -40,10 +57,16 @@ export const retrieveServerOlmAccount = query({
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const olmAccount = await ctx.db.get<"olmAccount">(args.userId as Id<"olmAccount">);
|
const olmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", args.userId)).first();
|
||||||
if (olmAccount) return olmAccount;
|
if (!olmAccount) return null;
|
||||||
|
|
||||||
return null;
|
// Ensure backward compatibility with old records that don't have keyVersion
|
||||||
|
return {
|
||||||
|
...olmAccount,
|
||||||
|
keyVersion: olmAccount.keyVersion ?? 1,
|
||||||
|
createdAt: olmAccount.createdAt ?? olmAccount._creationTime,
|
||||||
|
updatedAt: olmAccount.updatedAt ?? olmAccount._creationTime,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,4 +95,49 @@ export const consumeOTK = mutation({
|
||||||
keysLeft: oneTimeKeys.length
|
keysLeft: oneTimeKeys.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
export const getKeyVersion = query({
|
||||||
|
args: {
|
||||||
|
userId: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const olmAccount = await ctx.db.query("olmAccount").withIndex("userId", (q) => q.eq("userId", args.userId)).first();
|
||||||
|
if (!olmAccount) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyVersion: olmAccount.keyVersion ?? 1,
|
||||||
|
updatedAt: olmAccount.updatedAt ?? olmAccount._creationTime,
|
||||||
|
identityKey: olmAccount.identityKey,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration mutation to add keyVersion, createdAt, updatedAt to existing olmAccount records
|
||||||
|
* Run this once to migrate old records
|
||||||
|
*/
|
||||||
|
export const migrateOlmAccounts = mutation({
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const accounts = await ctx.db.query("olmAccount").collect();
|
||||||
|
let updated = 0;
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
// Only update if keyVersion is missing
|
||||||
|
if (account.keyVersion === undefined) {
|
||||||
|
await ctx.db.patch(account._id, {
|
||||||
|
keyVersion: 1, // Initial version for existing accounts
|
||||||
|
createdAt: account.createdAt ?? account._creationTime,
|
||||||
|
updatedAt: account.updatedAt ?? account._creationTime,
|
||||||
|
});
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: `Migrated ${updated} olmAccount records`,
|
||||||
|
total: accounts.length,
|
||||||
|
updated
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import { defineSchema, defineTable } from "convex/server";
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
import { nests } from "./schemas/nests";
|
||||||
import { user } from "./schemas/user";
|
import { user } from "./schemas/user";
|
||||||
|
|
||||||
const Attachment = v.object({
|
const Attachment = v.object({
|
||||||
|
|
@ -39,7 +40,12 @@ const Message = v.object({
|
||||||
|
|
||||||
export const tables = {
|
export const tables = {
|
||||||
...user,
|
...user,
|
||||||
messages: defineTable(Message),
|
...nests,
|
||||||
|
messages: defineTable(Message)
|
||||||
|
.index("channelId", ["channelId"])
|
||||||
|
.index("channelId_createdTimestamp", ["channelId", "createdTimestamp"])
|
||||||
|
.index("authorId", ["authorId"])
|
||||||
|
.index("guildId", ["guildId"]),
|
||||||
attachments: defineTable(Attachment),
|
attachments: defineTable(Attachment),
|
||||||
session: defineTable({
|
session: defineTable({
|
||||||
expiresAt: v.number(),
|
expiresAt: v.number(),
|
||||||
|
|
@ -96,6 +102,9 @@ export const tables = {
|
||||||
keyId: v.string(),
|
keyId: v.string(),
|
||||||
publicKey: v.string(),
|
publicKey: v.string(),
|
||||||
})),
|
})),
|
||||||
|
createdAt: v.optional(v.number()),
|
||||||
|
updatedAt: v.optional(v.number()),
|
||||||
|
keyVersion: v.optional(v.number()), // Increments when keys are rotated
|
||||||
})
|
})
|
||||||
.index("userId", ["userId"])
|
.index("userId", ["userId"])
|
||||||
.index("userId_keys", ["userId", "oneTimeKeys"])
|
.index("userId_keys", ["userId", "oneTimeKeys"])
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ export const nests = {
|
||||||
type: v.union(v.literal("global"), v.literal("regional"), v.literal("private")),
|
type: v.union(v.literal("global"), v.literal("regional"), v.literal("private")),
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
description: v.optional(v.string()),
|
description: v.optional(v.string()),
|
||||||
images: v.object({
|
images: v.optional(
|
||||||
banner: v.id("storage"),
|
v.object({
|
||||||
icon: v.id("storage"),
|
banner: v.id("storage"),
|
||||||
}),
|
icon: v.id("storage"),
|
||||||
|
})
|
||||||
|
),
|
||||||
colors: v.optional(
|
colors: v.optional(
|
||||||
v.object({
|
v.object({
|
||||||
primary: v.string(),
|
primary: v.string(),
|
||||||
|
|
@ -28,11 +30,13 @@ export const nests = {
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
})),
|
})),
|
||||||
|
onDiscover: v.optional(v.boolean()),
|
||||||
})
|
})
|
||||||
.index("managerId", ["managerId"])
|
.index("managerId", ["managerId"])
|
||||||
.index("type", ["type"])
|
.index("type", ["type"])
|
||||||
.index("type_region", ["type", "region"])
|
.index("type_region", ["type", "region"])
|
||||||
.index("createdAt", ["createdAt"]),
|
.index("createdAt", ["createdAt"])
|
||||||
|
.index("onDiscover", ["onDiscover"]),
|
||||||
roles: defineTable({
|
roles: defineTable({
|
||||||
nestId: v.id("nests"),
|
nestId: v.id("nests"),
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
|
|
@ -45,9 +49,12 @@ export const nests = {
|
||||||
flags: v.array(v.int64()), // Flags as bitfield
|
flags: v.array(v.int64()), // Flags as bitfield
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
|
members: v.array(v.id("user")),
|
||||||
})
|
})
|
||||||
.index("nestId", ["nestId"])
|
.index("nestId", ["nestId"])
|
||||||
.index("nestId_position", ["nestId", "position"]),
|
.index("nestId_position", ["nestId", "position"])
|
||||||
|
.index("nestId_members", ["nestId", "members"])
|
||||||
|
.index("members", ["members"]),
|
||||||
channels: defineTable({
|
channels: defineTable({
|
||||||
type: v.union(v.literal("text"), v.literal("category"), v.literal("announcement")),
|
type: v.union(v.literal("text"), v.literal("category"), v.literal("announcement")),
|
||||||
name: v.string(),
|
name: v.string(),
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,23 @@ export const user = {
|
||||||
metadata: v.optional(v.object({
|
metadata: v.optional(v.object({
|
||||||
phrasePreference: v.union(v.literal("comforting"), v.literal("mocking"), v.literal("both")),
|
phrasePreference: v.union(v.literal("comforting"), v.literal("mocking"), v.literal("both")),
|
||||||
})),
|
})),
|
||||||
|
nests: v.optional(v.array(v.id("nests"))),
|
||||||
})
|
})
|
||||||
.index("email_name", ["email", "name"])
|
.index("email_name", ["email", "name"])
|
||||||
|
.index("nests", ["nests"])
|
||||||
.index("byName", ["name"])
|
.index("byName", ["name"])
|
||||||
.index("userId", ["userId"])
|
.index("userId", ["userId"])
|
||||||
.index("username", ["username"]),
|
.index("username", ["username"]),
|
||||||
userStatus: defineTable({
|
userStatus: defineTable({
|
||||||
userId: v.id("user"),
|
userId: v.id("user"),
|
||||||
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
isUserSet: v.boolean(),
|
userSetStatus: v.optional(
|
||||||
|
v.object({
|
||||||
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
isSet: v.boolean(),
|
||||||
|
})
|
||||||
|
),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
})
|
})
|
||||||
.index("userId", ["userId"])
|
.index("userId", ["userId"])
|
||||||
|
|
@ -42,6 +50,7 @@ export const user = {
|
||||||
.index("userId_method", ["userId", "method"])
|
.index("userId_method", ["userId", "method"])
|
||||||
.index("userId", ["userId"])
|
.index("userId", ["userId"])
|
||||||
.index("requestId", ["requestId"])
|
.index("requestId", ["requestId"])
|
||||||
|
.index("userId_requestTo", ["userId", "requestTo"])
|
||||||
.index("requestTo", ["requestTo"])
|
.index("requestTo", ["requestTo"])
|
||||||
.index("expiresAt", ["expiresAt"]),
|
.index("expiresAt", ["expiresAt"]),
|
||||||
friends: defineTable({
|
friends: defineTable({
|
||||||
|
|
|
||||||
|
|
@ -28,25 +28,38 @@ async function userValidation(ctx: MutationCtx | QueryCtx, options?: { required?
|
||||||
export const updateUserStatus = mutation({
|
export const updateUserStatus = mutation({
|
||||||
args: {
|
args: {
|
||||||
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
status: v.union(v.literal("online"), v.literal("busy"), v.literal("offline"), v.literal("away")),
|
||||||
isUserSet: v.boolean(),
|
isUserSet: v.optional(v.boolean()),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
try {
|
try {
|
||||||
const { userId } = await userValidation(ctx);
|
const { userId } = await userValidation(ctx);
|
||||||
|
const isUserSet = args.isUserSet ?? false;
|
||||||
|
|
||||||
// Check if user status is already set
|
|
||||||
const userStatus = await ctx.db.query("userStatus").withIndex("userId", (q) => q.eq("userId", userId)).first();
|
const userStatus = await ctx.db.query("userStatus").withIndex("userId", (q) => q.eq("userId", userId)).first();
|
||||||
if (userStatus) {
|
if (userStatus) {
|
||||||
|
let resolvedStatus = args.status;
|
||||||
|
|
||||||
|
// Restore user-set status when reconnecting
|
||||||
|
if (args.status === "online" && !isUserSet && userStatus.userSetStatus?.isSet) {
|
||||||
|
resolvedStatus = userStatus.userSetStatus.status;
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.db.patch(userStatus._id, {
|
await ctx.db.patch(userStatus._id, {
|
||||||
status: args.status,
|
status: resolvedStatus,
|
||||||
isUserSet: args.isUserSet,
|
userSetStatus: isUserSet
|
||||||
|
? { status: args.status, updatedAt: Date.now(), isSet: true }
|
||||||
|
: userStatus.userSetStatus,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await ctx.db.insert("userStatus", {
|
await ctx.db.insert("userStatus", {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
status: args.status,
|
status: args.status,
|
||||||
isUserSet: false,
|
userSetStatus: {
|
||||||
|
status: args.status,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
isSet: isUserSet,
|
||||||
|
},
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +84,59 @@ export const getUserStatus = query({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getNonOfflineUserIds = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const results: { userId: string; status: string; isUserSet: boolean }[] = [];
|
||||||
|
|
||||||
|
for (const status of ["online", "busy", "away"] as const) {
|
||||||
|
const records = await ctx.db
|
||||||
|
.query("userStatus")
|
||||||
|
.withIndex("status", (q) => q.eq("status", status))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
results.push({
|
||||||
|
userId: record.userId,
|
||||||
|
status: record.status,
|
||||||
|
isUserSet: record.userSetStatus?.isSet ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const forceUserOffline = mutation({
|
||||||
|
args: {
|
||||||
|
userId: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const normalizedId = ctx.db.normalizeId("user", args.userId);
|
||||||
|
if (!normalizedId) return;
|
||||||
|
|
||||||
|
const userStatus = await ctx.db
|
||||||
|
.query("userStatus")
|
||||||
|
.withIndex("userId", (q) => q.eq("userId", normalizedId))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!userStatus || userStatus.status === "offline") return;
|
||||||
|
|
||||||
|
await ctx.db.patch(userStatus._id, {
|
||||||
|
status: "offline",
|
||||||
|
userSetStatus: userStatus.userSetStatus ? {
|
||||||
|
status: userStatus.userSetStatus.status,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
isSet: userStatus.userSetStatus.isSet,
|
||||||
|
} : undefined,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[forceUserOffline] Set user ${args.userId} offline (was: ${userStatus.status})`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const updateUserMetadata = mutation({
|
export const updateUserMetadata = mutation({
|
||||||
args: {
|
args: {
|
||||||
metadata: v.object({
|
metadata: v.object({
|
||||||
|
|
@ -320,7 +386,7 @@ export const getFriends = query({
|
||||||
friendshipCreatedAt: friendship.createdAt,
|
friendshipCreatedAt: friendship.createdAt,
|
||||||
status: friendStatus ? {
|
status: friendStatus ? {
|
||||||
status: friendStatus.status,
|
status: friendStatus.status,
|
||||||
isUserSet: friendStatus.isUserSet,
|
isUserSet: friendStatus.userSetStatus?.isSet ?? false,
|
||||||
} : {
|
} : {
|
||||||
status: "offline" as const,
|
status: "offline" as const,
|
||||||
isUserSet: false,
|
isUserSet: false,
|
||||||
|
|
@ -356,6 +422,14 @@ export const getParticipantDetails = query({
|
||||||
const participantOlmAccount = await ctx.db.query("olmAccount").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;
|
if (!participant) return null;
|
||||||
|
|
||||||
|
// Ensure backward compatibility with old olmAccount records
|
||||||
|
const olmAccountWithDefaults = participantOlmAccount ? {
|
||||||
|
...participantOlmAccount,
|
||||||
|
keyVersion: participantOlmAccount.keyVersion ?? 1,
|
||||||
|
createdAt: participantOlmAccount.createdAt ?? participantOlmAccount._creationTime,
|
||||||
|
updatedAt: participantOlmAccount.updatedAt ?? participantOlmAccount._creationTime,
|
||||||
|
} : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: participant._id,
|
id: participant._id,
|
||||||
name: participant.name,
|
name: participant.name,
|
||||||
|
|
@ -363,7 +437,7 @@ export const getParticipantDetails = query({
|
||||||
displayUsername: participant.displayUsername,
|
displayUsername: participant.displayUsername,
|
||||||
image: participant.image,
|
image: participant.image,
|
||||||
status: participantStatus?.status || "offline",
|
status: participantStatus?.status || "offline",
|
||||||
olmAccount: participantOlmAccount,
|
olmAccount: olmAccountWithDefaults,
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
25
package.json
25
package.json
|
|
@ -5,12 +5,14 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "bun src/lib/scripts/copy-olm.ts",
|
"postinstall": "bun src/lib/scripts/copy-olm.ts",
|
||||||
"dev": "cross-env NODE_ENV=development PORT=3000 tsx src/server.ts",
|
"dev": "cross-env NODE_ENV=development PORT=3000 tsx src/server.ts",
|
||||||
"build": "bun src/lib/scripts/copy-olm.ts && convex deploy --cmd \"bun run build\"",
|
"build": "bun src/lib/scripts/copy-olm.ts && convex deploy --cmd \"next build\"",
|
||||||
"start": "cross-env NODE_ENV=production PORT=8081 tsx src/server.ts"
|
"build:local": "bun src/lib/scripts/copy-olm.ts && next build",
|
||||||
|
"start": "cross-env NODE_ENV=production PORT=8081 tsx src/server.ts",
|
||||||
|
"start:dev": "cross-env NODE_ENV=development PORT=3000 tsx src/server.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/better-auth": "^0.10.10",
|
"@convex-dev/better-auth": "^0.10.10",
|
||||||
"@marsidev/react-turnstile": "^1.4.1",
|
"@marsidev/react-turnstile": "^1.4.2",
|
||||||
"@matrix-org/olm": "^3.2.15",
|
"@matrix-org/olm": "^3.2.15",
|
||||||
"@nanostores/react": "^1.0.0",
|
"@nanostores/react": "^1.0.0",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
|
|
@ -27,36 +29,37 @@
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/libsodium-wrappers": "^0.7.14",
|
"@types/libsodium-wrappers": "^0.7.14",
|
||||||
"better-auth": "1.4.12",
|
"better-auth": "1.4.12",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"convex": "^1.31.4",
|
"convex": "^1.31.7",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.3.0",
|
||||||
"dexie-react-hooks": "^4.2.0",
|
"dexie-react-hooks": "^4.2.0",
|
||||||
"framer-motion": "^12.26.2",
|
"dotenv": "^17.3.1",
|
||||||
|
"framer-motion": "^12.34.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nanostores": "^1.1.0",
|
"nanostores": "^1.1.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-day-picker": "^9.13.0",
|
"react-day-picker": "^9.13.1",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^25.0.8",
|
"@types/node": "^25.2.2",
|
||||||
"@types/react": "^19.2.8",
|
"@types/react": "^19.2.13",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
|
|
||||||
3
src/app/(app)/channels/me/friends/page.tsx
Normal file
3
src/app/(app)/channels/me/friends/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function FriendsPage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
3
src/app/(app)/channels/nests/global/page.tsx
Normal file
3
src/app/(app)/channels/nests/global/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function GlobalNestsPage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export default function ServerChannelPage() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
3
src/app/(app)/discover/page.tsx
Normal file
3
src/app/(app)/discover/page.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function DiscoverPage() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AutoRequestNotifications } from "@/components/notifications/NotificationSettings";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { getToken } from "@/lib/auth/auth-server";
|
import { getToken } from "@/lib/auth/auth-server";
|
||||||
|
|
@ -49,6 +50,7 @@ export default async function RootLayout({
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<AutoRequestNotifications />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
</ConvexClientProvider>
|
</ConvexClientProvider>
|
||||||
|
|
|
||||||
|
|
@ -15,27 +15,59 @@ import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { api } from "../../convex/_generated/api";
|
import { api } from "../../convex/_generated/api";
|
||||||
import OlmPasswordDialog from "./olm/olm-password-dialog";
|
import OlmPasswordDialog from "./olm/olm-password-dialog";
|
||||||
|
|
||||||
|
type RouteParams = Record<string, string | string[] | undefined>;
|
||||||
|
|
||||||
|
type RouteMatcher = {
|
||||||
|
path?: string;
|
||||||
|
pattern?: RegExp;
|
||||||
|
type: SiPher.PageTypes;
|
||||||
|
extract?: (match: RegExpMatchArray, params: RouteParams) => Partial<SiPher.RouteInfo>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routes: RouteMatcher[] = [
|
||||||
|
{ path: '/channels/me/friends', type: 'friends' },
|
||||||
|
{ path: '/discover', type: 'discover' },
|
||||||
|
{ path: '/support', type: 'support' },
|
||||||
|
{ path: '/channels/nests/global', type: 'global-nests' },
|
||||||
|
{
|
||||||
|
pattern: /^\/channels\/me\/(.+)$/,
|
||||||
|
type: 'dm',
|
||||||
|
extract: (_, params) => ({
|
||||||
|
dmChannelId: params.id ? decodeURIComponent(params.id as string) : undefined
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^\/channels\/servers\/(.+)$/,
|
||||||
|
type: 'server',
|
||||||
|
extract: (_, params) => ({
|
||||||
|
serverId: params.serverId ? decodeURIComponent(params.serverId as string) : undefined,
|
||||||
|
serverChannelId: params.channelId ? decodeURIComponent(params.channelId as string) : undefined
|
||||||
|
})
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function AppContainerContent() {
|
function AppContainerContent() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
// Detect route type and extract params from URL
|
const routeInfo: SiPher.RouteInfo = useMemo(() => {
|
||||||
const routeInfo = useMemo(() => {
|
for (const route of routes) {
|
||||||
if (pathname.startsWith('/channels/me/')) {
|
if (route.path && pathname === route.path) {
|
||||||
return {
|
return { type: route.type };
|
||||||
type: 'dm' as const,
|
}
|
||||||
// Decode URL-encoded params (dm%3A... becomes dm:...)
|
|
||||||
dmChannelId: params.id ? decodeURIComponent(params.id as string) : undefined
|
if (route.pattern) {
|
||||||
};
|
const match = pathname.match(route.pattern);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: route.type,
|
||||||
|
...route.extract?.(match, params)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pathname.startsWith('/channels/servers/')) {
|
|
||||||
return {
|
return { type: 'friends' };
|
||||||
type: 'server' as const,
|
|
||||||
serverId: params.serverId ? decodeURIComponent(params.serverId as string) : undefined,
|
|
||||||
serverChannelId: params.channelId ? decodeURIComponent(params.channelId as string) : undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { type: 'home' as const };
|
|
||||||
}, [pathname, params]);
|
}, [pathname, params]);
|
||||||
|
|
||||||
const { data } = authClient.useSession();
|
const { data } = authClient.useSession();
|
||||||
|
|
@ -47,6 +79,7 @@ function AppContainerContent() {
|
||||||
const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmContext();
|
const { olmStatus, showOlmModal, setShowOlmModal, handleCreateAccount } = useOlmContext();
|
||||||
|
|
||||||
const updateUserMetadata = useMutation(api.auth.updateUserMetadata);
|
const updateUserMetadata = useMutation(api.auth.updateUserMetadata);
|
||||||
|
const userNests = useQuery(api.auth.getUserNests);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
@ -82,15 +115,15 @@ function AppContainerContent() {
|
||||||
socketInfo={socketInfo}
|
socketInfo={socketInfo}
|
||||||
disconnectSocket={disconnect}
|
disconnectSocket={disconnect}
|
||||||
connectSocket={connect}
|
connectSocket={connect}
|
||||||
|
routeInfo={routeInfo}
|
||||||
>
|
>
|
||||||
<MainContentLayout
|
<MainContentLayout
|
||||||
socketStatus={socketStatus}
|
socketStatus={socketStatus}
|
||||||
emptyChannelMessage={getPhrase()}
|
emptyChannelMessage={getPhrase()}
|
||||||
emptyFriendsMessage={getPhrase()}
|
emptyFriendsMessage={getPhrase()}
|
||||||
userId={data.user.id}
|
userId={data.user.id}
|
||||||
dmChannelId={routeInfo.type === 'dm' ? routeInfo.dmChannelId : undefined}
|
routeInfo={routeInfo}
|
||||||
serverId={routeInfo.type === 'server' ? routeInfo.serverId : undefined}
|
userNests={userNests}
|
||||||
serverChannelId={routeInfo.type === 'server' ? routeInfo.serverChannelId : undefined}
|
|
||||||
/>
|
/>
|
||||||
</AppSidebar>
|
</AppSidebar>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import {
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarProvider
|
SidebarProvider
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { CompassIcon, HouseIcon } from "@phosphor-icons/react";
|
import { CompassIcon, GlobeIcon, HouseIcon } from "@phosphor-icons/react";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
|
|
@ -19,10 +20,17 @@ import ConnectionStatusIndicator from "./csi";
|
||||||
import SidebarIcon from "./sicons";
|
import SidebarIcon from "./sicons";
|
||||||
|
|
||||||
const SidebarItems: SiPher.SidebarItem[] = [
|
const SidebarItems: SiPher.SidebarItem[] = [
|
||||||
|
{
|
||||||
|
id: "global-nests",
|
||||||
|
icon: <GlobeIcon className="size-5" weight="fill" />,
|
||||||
|
label: "Global Nest",
|
||||||
|
href: "/channels/nests/global"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "discover",
|
id: "discover",
|
||||||
icon: <CompassIcon className="size-5" weight="fill" />,
|
icon: <CompassIcon className="size-5" weight="fill" />,
|
||||||
label: "Discover"
|
label: "Discover",
|
||||||
|
href: "/discover"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -31,8 +39,9 @@ const SidebarItems: SiPher.SidebarItem[] = [
|
||||||
* It also is the controller for everything on the app, including going to other pages, showing conversations and other.
|
* It also is the controller for everything on the app, including going to other pages, showing conversations and other.
|
||||||
* @param children - The children to be rendered in the sidebar inset
|
* @param children - The children to be rendered in the sidebar inset
|
||||||
*/
|
*/
|
||||||
export default function AppSidebar({ children, socketStatus, socketInfo, currentChannel, disconnectSocket, connectSocket }: SiPher.AppSidebarProps) {
|
export default function AppSidebar({ children, socketStatus, socketInfo, currentChannel, disconnectSocket, connectSocket, routeInfo }: SiPher.AppSidebarProps) {
|
||||||
const [activeItem, setActiveItem] = useState<string>("home");
|
const [activeItem, setActiveItem] = useState<string>("home");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
|
|
@ -61,9 +70,13 @@ export default function AppSidebar({ children, socketStatus, socketInfo, current
|
||||||
<SidebarContent className="pt-2 px-0 overflow-hidden">
|
<SidebarContent className="pt-2 px-0 overflow-hidden">
|
||||||
<SidebarMenu className="gap-2">
|
<SidebarMenu className="gap-2">
|
||||||
{SidebarItems.map((item) => (
|
{SidebarItems.map((item) => (
|
||||||
<SidebarMenuItem key={item.id}>
|
<SidebarMenuItem key={item.id} onClick={() => {
|
||||||
|
if (item.href) {
|
||||||
|
router.push(item.href);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<SidebarIcon
|
<SidebarIcon
|
||||||
isActive={activeItem === item.id}
|
isActive={activeItem === item.id && routeInfo.type === item.id}
|
||||||
label={item.label}
|
label={item.label}
|
||||||
onClick={() => setActiveItem(item.id)}
|
onClick={() => setActiveItem(item.id)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
99
src/components/notifications/NotificationSettings.tsx
Normal file
99
src/components/notifications/NotificationSettings.tsx
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { requestNotificationPermission } from "@/lib/notifications";
|
||||||
|
import { Bell, BellOff } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export function NotificationSettings({ userStatus }: { userStatus: "online" | "busy" | "offline" | "away" }) {
|
||||||
|
const [permission, setPermission] = useState<NotificationPermission>("default");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ("Notification" in window) {
|
||||||
|
setPermission(Notification.permission);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRequestPermission = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const newPermission = await requestNotificationPermission();
|
||||||
|
setPermission(newPermission);
|
||||||
|
|
||||||
|
if (newPermission === "granted") {
|
||||||
|
if (userStatus === "busy") return;
|
||||||
|
// Show a test notification
|
||||||
|
new Notification("Notifications enabled!", {
|
||||||
|
body: "You'll now receive message notifications",
|
||||||
|
icon: "/logo.png",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to request notification permission:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!("Notification" in window)) {
|
||||||
|
return null; // Browser doesn't support notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "granted") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Bell className="h-4 w-4 text-green-500" />
|
||||||
|
<span>Notifications enabled</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "denied") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<BellOff className="h-4 w-4 text-red-500" />
|
||||||
|
<span>Notifications blocked. Enable in browser settings.</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRequestPermission}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
|
{isLoading ? "Requesting..." : "Enable Notifications"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-request notification permission component
|
||||||
|
* Place in your app layout to automatically request permission on load
|
||||||
|
*/
|
||||||
|
export function AutoRequestNotifications() {
|
||||||
|
useEffect(() => {
|
||||||
|
if ("Notification" in window && Notification.permission === "default") {
|
||||||
|
// Auto-request permission after a short delay (to not interrupt page load)
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const permission = await requestNotificationPermission();
|
||||||
|
if (permission === "granted") {
|
||||||
|
console.log("[Notifications] Permission granted automatically");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("[Notifications] Auto-request failed or was dismissed:", error);
|
||||||
|
}
|
||||||
|
}, 2000); // Wait 2 seconds after page load
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null; // This component doesn't render anything
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useOlmContext } from "@/contexts/olm-context";
|
import { useOlmContext } from "@/contexts/olm-context";
|
||||||
import { useSocketContext } from "@/contexts/socket-context";
|
import { useSocketContext } from "@/contexts/socket-context";
|
||||||
import { clearUnread, db, sendMessage } from "@/lib/db";
|
import { clearUnread, db, sendMessage } from "@/lib/db";
|
||||||
|
import { setActiveChannel } from "@/lib/notifications";
|
||||||
import { useLiveQuery } from "dexie-react-hooks";
|
import { useLiveQuery } from "dexie-react-hooks";
|
||||||
import { KeyRound, SendIcon } from "lucide-react";
|
import { KeyRound, SendIcon } from "lucide-react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
@ -111,10 +112,21 @@ export default function DMChannelContent(
|
||||||
}
|
}
|
||||||
}, [allMessages.length]);
|
}, [allMessages.length]);
|
||||||
|
|
||||||
// Clear unread count when entering the channel
|
// Set active channel and clear unread count when viewing this channel
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Mark this channel as active (prevents notifications)
|
||||||
|
setActiveChannel(channelId);
|
||||||
|
|
||||||
|
// Clear any existing unread count
|
||||||
clearUnread(channelId);
|
clearUnread(channelId);
|
||||||
console.debug("[DMChannelContent] Cleared unread count for channel", channelId);
|
|
||||||
|
console.debug("[DMChannelContent] Set active channel and cleared unread:", channelId);
|
||||||
|
|
||||||
|
// Cleanup: unset active channel when leaving
|
||||||
|
return () => {
|
||||||
|
setActiveChannel(null);
|
||||||
|
console.debug("[DMChannelContent] Cleared active channel");
|
||||||
|
};
|
||||||
}, [channelId]);
|
}, [channelId]);
|
||||||
|
|
||||||
// Guard: Check if otherUser exists
|
// Guard: Check if otherUser exists
|
||||||
|
|
@ -131,18 +143,31 @@ export default function DMChannelContent(
|
||||||
// Get or create session when OLM is ready and we have the other user's account
|
// Get or create session when OLM is ready and we have the other user's account
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSession = async () => {
|
const loadSession = async () => {
|
||||||
|
console.log("[DMChannelContent] loadSession effect triggered", {
|
||||||
|
isReady,
|
||||||
|
hasOlmAccount: !!olmAccount,
|
||||||
|
hasOtherUser: !!otherUser,
|
||||||
|
hasOtherUserOlmAccount: !!otherUser?.olmAccount,
|
||||||
|
otherUserId: otherUser?.id
|
||||||
|
});
|
||||||
|
|
||||||
if (!isReady || !olmAccount || !otherUser || !otherUser.olmAccount) {
|
if (!isReady || !olmAccount || !otherUser || !otherUser.olmAccount) {
|
||||||
|
console.log("[DMChannelContent] Not ready to load session, skipping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionError(null);
|
setSessionError(null);
|
||||||
|
console.log("[DMChannelContent] Calling getSession for", otherUser.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await getSession(otherUser.id, otherUser.olmAccount);
|
const session = await getSession(otherUser.id, otherUser.olmAccount);
|
||||||
|
console.log("[DMChannelContent] getSession returned:", !!session);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
setOlmSession(session);
|
setOlmSession(session);
|
||||||
|
console.log("[DMChannelContent] Session set successfully");
|
||||||
} else {
|
} else {
|
||||||
|
console.error("[DMChannelContent] getSession returned null");
|
||||||
setSessionError("Failed to create encryption session");
|
setSessionError("Failed to create encryption session");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -152,7 +177,7 @@ export default function DMChannelContent(
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSession();
|
loadSession();
|
||||||
}, [isReady, olmAccount, otherUser, password,])
|
}, [isReady, olmAccount, otherUser, password, getSession])
|
||||||
|
|
||||||
// Check if OLM is ready
|
// Check if OLM is ready
|
||||||
if (!isReady || !olmAccount) {
|
if (!isReady || !olmAccount) {
|
||||||
|
|
@ -263,6 +288,7 @@ export default function DMChannelContent(
|
||||||
const displayName = isSelf ? selfDetail?.displayUsername ?? selfDetail?.username ?? selfDetail?.name ?? "You" : (sender?.displayUsername ?? sender?.username ?? sender?.name ?? "Unknown");
|
const displayName = isSelf ? selfDetail?.displayUsername ?? selfDetail?.username ?? selfDetail?.name ?? "You" : (sender?.displayUsername ?? sender?.username ?? sender?.name ?? "Unknown");
|
||||||
const timestamp = moment(msg.timestamp);
|
const timestamp = moment(msg.timestamp);
|
||||||
const timeLabel = timestamp.isSame(moment(), "day") ? timestamp.format("h:mm A") : timestamp.format("MMM D, YYYY h:mm A");
|
const timeLabel = timestamp.isSame(moment(), "day") ? timestamp.format("h:mm A") : timestamp.format("MMM D, YYYY h:mm A");
|
||||||
|
const shortTimeLabel = timestamp.format("h:mm A")
|
||||||
|
|
||||||
// Check if this message is from the same user as the previous one within 5 minutes
|
// 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 prevMsg = index > 0 ? messages[index - 1] : null;
|
||||||
|
|
@ -303,11 +329,9 @@ export default function DMChannelContent(
|
||||||
) : (
|
) : (
|
||||||
// Compact message without avatar (grouped)
|
// Compact message without avatar (grouped)
|
||||||
<div className="flex gap-2 md:gap-4 leading-5.5">
|
<div className="flex gap-2 md:gap-4 leading-5.5">
|
||||||
<div className="w-8 md:w-10 shrink-0 flex items-start justify-end pt-0.5">
|
<div className="w-6 md:w-10 shrink-0 flex items-start justify-end pt-0.5">
|
||||||
<span className="text-[10px] text-transparent group-hover:text-muted-foreground transition-colors duration-100 font-medium">
|
<span className="text-[9px] text-transparent group-hover:text-muted-foreground transition-colors duration-100 font-light">
|
||||||
{
|
{shortTimeLabel}
|
||||||
timeLabel
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 text-sm md:text-[15px] leading-5.5 text-foreground wrap-break-word">
|
<div className="flex-1 min-w-0 text-sm md:text-[15px] leading-5.5 text-foreground wrap-break-word">
|
||||||
|
|
@ -339,6 +363,13 @@ export default function DMChannelContent(
|
||||||
onKeyDown={async (e) => {
|
onKeyDown={async (e) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey && messageInput.trim() && password) {
|
if (e.key === 'Enter' && !e.shiftKey && messageInput.trim() && password) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log("[DMChannelContent] Attempting to send message", {
|
||||||
|
hasOlmSession: !!olmSession,
|
||||||
|
hasPassword: !!password,
|
||||||
|
recipientId: otherUser.id,
|
||||||
|
recipientKeyVersion: otherUser.olmAccount?.keyVersion
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageId = await sendMessage({
|
const messageId = await sendMessage({
|
||||||
channelId,
|
channelId,
|
||||||
|
|
@ -351,8 +382,12 @@ export default function DMChannelContent(
|
||||||
userId,
|
userId,
|
||||||
recipientId: otherUser.id,
|
recipientId: otherUser.id,
|
||||||
password,
|
password,
|
||||||
|
recipientKeyVersion: otherUser.olmAccount?.keyVersion,
|
||||||
|
recipientIdentityKey: otherUser.olmAccount?.identityKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("[DMChannelContent] Message sent successfully, ID:", messageId);
|
||||||
|
|
||||||
if (messageId) {
|
if (messageId) {
|
||||||
setMessageInput("");
|
setMessageInput("");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,56 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import { clearUnread, db } from "@/lib/db"
|
import { clearUnread, db } from "@/lib/db"
|
||||||
import { QuestionMarkIcon } from "@phosphor-icons/react"
|
import { QuestionMarkIcon } from "@phosphor-icons/react"
|
||||||
import { formatDistanceToNow } from "date-fns"
|
import { formatDistanceToNow } from "date-fns"
|
||||||
import { useLiveQuery } from "dexie-react-hooks"
|
import { useLiveQuery } from "dexie-react-hooks"
|
||||||
import { MessageSquarePlusIcon, SettingsIcon, UsersIcon, XIcon } from "lucide-react"
|
import { Globe2Icon, GlobeIcon, HomeIcon, MessageSquarePlusIcon, SettingsIcon, UsersIcon, XIcon } from "lucide-react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useMemo } from "react"
|
||||||
import UserCard from "../user/user-card"
|
import UserCard from "../user/user-card"
|
||||||
|
|
||||||
|
// Mock channels for testing scroll behavior - set to true to enable
|
||||||
|
const ENABLE_MOCK_CHANNELS = true
|
||||||
|
|
||||||
|
function createMockChannel(id: string, name: string, message: string, hoursAgo: number): SiPher.Channel {
|
||||||
|
return {
|
||||||
|
id: `mock-${id}`,
|
||||||
|
name,
|
||||||
|
type: "DM" as SiPher.Channel["type"],
|
||||||
|
participants: ["current-user", `user-${id}`],
|
||||||
|
isOpen: true,
|
||||||
|
metadata: {},
|
||||||
|
times: {
|
||||||
|
createdAt: Date.now() - 1000 * 60 * 60 * 24 * 30,
|
||||||
|
updatedAt: Date.now() - 1000 * 60 * 60 * hoursAgo,
|
||||||
|
lastMessage: { content: message } as unknown as SiPher.Channel["times"]["lastMessage"],
|
||||||
|
lastMessageAt: Date.now() - 1000 * 60 * 60 * hoursAgo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const mockChannels: SiPher.Channel[] = ENABLE_MOCK_CHANNELS ? [
|
||||||
|
// createMockChannel("1", "Alice Johnson", "Hey, are you coming to the meeting?", 0.08),
|
||||||
|
// createMockChannel("2", "Bob Smith", "The project looks great!", 0.5),
|
||||||
|
// createMockChannel("3", "Charlie Brown", "Can you review my PR?", 1),
|
||||||
|
// createMockChannel("4", "Diana Prince", "Thanks for the help!", 2),
|
||||||
|
// createMockChannel("5", "Edward Norton", "Let's catch up soon", 5),
|
||||||
|
// createMockChannel("6", "Fiona Green", "Did you see the news?", 12),
|
||||||
|
// createMockChannel("7", "George Wilson", "Meeting at 3pm", 24),
|
||||||
|
// createMockChannel("8", "Hannah Baker", "Sounds good to me!", 48),
|
||||||
|
// createMockChannel("9", "Ivan Petrov", "I'll send over the files", 72),
|
||||||
|
// createMockChannel("10", "Julia Roberts", "Great work on that!", 96),
|
||||||
|
// createMockChannel("11", "Kevin Hart", "LOL that's hilarious", 120),
|
||||||
|
// createMockChannel("12", "Laura Palmer", "See you tomorrow", 144),
|
||||||
|
// ] : []
|
||||||
|
|
||||||
export interface ChannelListProps {
|
export interface ChannelListProps {
|
||||||
currentChannel: SiPher.Channel | null
|
currentChannel: SiPher.Channel | null
|
||||||
openDmChannels: SiPher.Channel[]
|
openDmChannels: SiPher.Channel[]
|
||||||
page: "friends" | "support" | "dm" | "server"
|
page: SiPher.PageTypes
|
||||||
onPageChange: (page: "friends" | "support" | "dm" | "server") => void
|
onPageChange: (page: SiPher.PageTypes) => void
|
||||||
emptyMessage?: string
|
emptyMessage?: string
|
||||||
dmChannel?: {
|
dmChannel?: {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -48,13 +85,22 @@ export function ChannelList({
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Combine real channels with mock channels for testing, sorted by most recent activity
|
||||||
|
const allDmChannels = useMemo(() => {
|
||||||
|
return [...openDmChannels].sort((a, b) => {
|
||||||
|
const aTime = a.times?.lastMessageAt ?? a.times?.updatedAt ?? 0
|
||||||
|
const bTime = b.times?.lastMessageAt ?? b.times?.updatedAt ?? 0
|
||||||
|
return bTime - aTime // Descending order (most recent first)
|
||||||
|
})
|
||||||
|
}, [openDmChannels])
|
||||||
|
|
||||||
const handleNavigation = (path: string) => {
|
const handleNavigation = (path: string) => {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
onChannelSelect?.()
|
onChannelSelect?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col shrink-0 border-border/40 ${isMobile ? 'w-full h-full bg-transparent' : 'max-w-72 min-w-72 border-r bg-linear-to-b from-background to-muted/20'}`}>
|
<div className={`flex flex-col shrink-0 border-border/40 ${isMobile ? 'w-full h-full bg-transparent' : 'max-w-72 min-w-72 h-full border-r bg-linear-to-b from-background to-muted/20'}`}>
|
||||||
{/* Channel List Header - Navigation Items (Desktop only) */}
|
{/* Channel List Header - Navigation Items (Desktop only) */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -67,7 +113,7 @@ export function ChannelList({
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPageChange("friends")
|
onPageChange("friends")
|
||||||
handleNavigation("/")
|
handleNavigation("/channels/me/friends")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${page === "friends"
|
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${page === "friends"
|
||||||
|
|
@ -137,11 +183,72 @@ export function ChannelList({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Channel List */}
|
{/* Channel List */}
|
||||||
<div className={`flex flex-col flex-1 overflow-y-auto ${isMobile ? 'px-2' : 'px-2'}`}>
|
<div className={`flex flex-col flex-1 min-h-0 ${isMobile ? 'px-2' : 'px-2'}`}>
|
||||||
{page === "friends" || !currentChannel ? (
|
<div className="flex flex-col w-full gap-2">
|
||||||
<div className="flex flex-col w-full">
|
<div className="flex items-center justify-between px-1 py-2 select-none">
|
||||||
|
<span className={`font-bold uppercase tracking-wider text-muted-foreground/70 ${isMobile ? 'text-[10px]' : 'text-[11px]'}`}>
|
||||||
|
Global Nests
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nest Type Selector */}
|
||||||
|
<div className={`flex ${isMobile ? 'flex-col gap-1' : 'flex-row gap-0.5'} p-1 bg-muted/40 rounded-lg`}>
|
||||||
|
{[
|
||||||
|
{ id: 'global', icon: GlobeIcon, label: 'Global', description: 'Worldwide nests' },
|
||||||
|
{ id: 'continental', icon: Globe2Icon, label: 'Continent', description: 'Nests by continent' },
|
||||||
|
{ id: 'country', icon: HomeIcon, label: 'Country', description: 'Your country nests' },
|
||||||
|
].map((nest) => {
|
||||||
|
const isActive = nest.id === 'global' // TODO: Replace with actual state
|
||||||
|
const Icon = nest.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={nest.id}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`
|
||||||
|
${isMobile
|
||||||
|
? 'w-full justify-start gap-3 h-11 px-3'
|
||||||
|
: 'flex-1 gap-1.5 h-7 px-2'
|
||||||
|
}
|
||||||
|
rounded-md transition-all duration-150
|
||||||
|
${isActive
|
||||||
|
? 'bg-background text-foreground shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground hover:bg-transparent'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
title={nest.description}
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Handle nest type selection
|
||||||
|
onChannelSelect?.()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<div className={`
|
||||||
|
flex items-center justify-center w-7 h-7 rounded-md transition-colors
|
||||||
|
${isActive ? 'bg-primary/15' : 'bg-muted/50'}
|
||||||
|
`}>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">{nest.label}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon className="size-3.5" />
|
||||||
|
<span className="text-[11px] font-medium">{nest.label}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(page === "friends" || !currentChannel) && (
|
||||||
|
<div className="flex flex-col w-full flex-1 min-h-0 overflow-hidden mb-16">
|
||||||
{/* DM Header */}
|
{/* DM Header */}
|
||||||
<div className="flex items-center justify-between px-1 py-2 select-none">
|
<div className="flex items-center justify-between px-1 py-2 select-none shrink-0">
|
||||||
<span className={`font-bold uppercase tracking-wider text-muted-foreground/70 ${isMobile ? 'text-[10px]' : 'text-[11px]'}`}>
|
<span className={`font-bold uppercase tracking-wider text-muted-foreground/70 ${isMobile ? 'text-[10px]' : 'text-[11px]'}`}>
|
||||||
Direct Messages
|
Direct Messages
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -155,88 +262,92 @@ export function ChannelList({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{openDmChannels.length > 0 ? (
|
{allDmChannels.length > 0 && (
|
||||||
<div className="flex flex-col gap-0.5">
|
<ScrollArea className="flex-1 -mx-2 h-full">
|
||||||
{openDmChannels.map((channel) => {
|
<div className="flex flex-col gap-0.5 px-2 pb-2">
|
||||||
const isActive = dmChannel?.id === channel.id
|
{allDmChannels.map((channel) => {
|
||||||
const lastMessage = channel.times?.lastMessage
|
const isActive = dmChannel?.id === channel.id
|
||||||
const lastMessageTime = channel.times?.lastMessageAt
|
const lastMessage = channel.times?.lastMessage
|
||||||
const channelUnreadCount = unreadCount?.find((unread) => unread.channelId === channel.id)?.count ?? 0
|
const lastMessageTime = channel.times?.lastMessageAt
|
||||||
if (!channel.isOpen) return null;
|
const channelUnreadCount = unreadCount?.find((unread) => unread.channelId === channel.id)?.count ?? 0
|
||||||
|
if (!channel.isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={channel.id}
|
key={channel.id}
|
||||||
className={`flex flex-row items-center gap-3 px-2 py-2.5 rounded-lg transition-all cursor-pointer group ${isActive
|
className={`flex flex-row items-center gap-3 px-2 py-2.5 rounded-lg transition-all cursor-pointer group ${isActive
|
||||||
? "bg-accent/80 shadow-sm ring-1 ring-accent"
|
? "bg-accent/80 shadow-sm ring-1 ring-accent"
|
||||||
: "hover:bg-accent/40 active:bg-accent/60"
|
: "hover:bg-accent/40 active:bg-accent/60"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clearUnread(channel.id)
|
clearUnread(channel.id)
|
||||||
console.log("Cleared unread count for channel", channel.id)
|
console.log("Cleared unread count for channel", channel.id)
|
||||||
handleNavigation(`/channels/me/${channel.id}`)
|
handleNavigation(`/channels/me/${channel.id}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative shrink-0">
|
<div className="relative shrink-0">
|
||||||
<UserCard
|
<UserCard
|
||||||
userName={channel.name}
|
userName={channel.name}
|
||||||
image={channel.metadata?.icon ?? undefined}
|
image={channel.metadata?.icon ?? undefined}
|
||||||
status={"none"}
|
status={"none"}
|
||||||
/>
|
/>
|
||||||
{channelUnreadCount > 0 && (
|
{channelUnreadCount > 0 && (
|
||||||
<span className="absolute -top-1 -right-1 flex items-center justify-center min-w-[18px] h-[18px] px-1.5 rounded-full bg-linear-to-br from-red-500 to-red-600 text-[10px] font-bold text-white shadow-md ring-2 ring-background">
|
<span className="absolute -top-1 -right-1 flex items-center justify-center min-w-[18px] h-[18px] px-1.5 rounded-full bg-linear-to-br from-red-500 to-red-600 text-[10px] font-bold text-white shadow-md ring-2 ring-background">
|
||||||
{channelUnreadCount > 99 ? '99+' : channelUnreadCount}
|
{channelUnreadCount > 99 ? '99+' : channelUnreadCount}
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Channel Info */}
|
|
||||||
<div className="flex flex-col justify-center flex-1 min-w-0 overflow-hidden">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className={`text-sm truncate ${isActive ? 'font-bold' : 'font-semibold'} text-foreground`}>
|
|
||||||
{channel.name}
|
|
||||||
</span>
|
|
||||||
{lastMessageTime && (
|
|
||||||
<span className="text-[10px] text-muted-foreground/60 shrink-0 font-medium">
|
|
||||||
{formatDistanceToNow(lastMessageTime, { addSuffix: false })}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{lastMessage && (
|
|
||||||
<span className="text-xs text-muted-foreground/70 truncate mt-0.5">
|
{/* Channel Info */}
|
||||||
{lastMessage.content}
|
<div className="flex flex-col justify-center flex-1 min-w-0 overflow-hidden">
|
||||||
</span>
|
<div className="flex items-center justify-between gap-2">
|
||||||
)}
|
<span className={`text-sm truncate ${isActive ? 'font-bold' : 'font-semibold'} text-foreground`}>
|
||||||
|
{channel.name}
|
||||||
|
</span>
|
||||||
|
{lastMessageTime && (
|
||||||
|
<span className="text-[10px] text-muted-foreground/60 shrink-0 font-medium">
|
||||||
|
{formatDistanceToNow(lastMessageTime, { addSuffix: false })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{lastMessage && (
|
||||||
|
<span className="text-xs text-muted-foreground/70 truncate mt-0.5">
|
||||||
|
{lastMessage.content}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Close button - always visible on mobile, hover-visible on desktop */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`size-7 p-0 shrink-0 hover:bg-destructive/10 hover:text-destructive rounded-md transition-all ${isMobile ? 'opacity-60' : 'opacity-0 group-hover:opacity-100'}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const isCurrentlyViewing = isActive
|
||||||
|
|
||||||
|
db.channels.where("id").equals(channel.id).modify((channel) => {
|
||||||
|
channel.isOpen = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate away if we're closing the currently viewed channel
|
||||||
|
if (isCurrentlyViewing) {
|
||||||
|
console.log("Navigating away from channel")
|
||||||
|
handleNavigation("/")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Close DM"
|
||||||
|
>
|
||||||
|
<XIcon className="size-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Close button - always visible on mobile, hover-visible on desktop */}
|
{allDmChannels.length === 0 && (
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={`size-7 p-0 shrink-0 hover:bg-destructive/10 hover:text-destructive rounded-md transition-all ${isMobile ? 'opacity-60' : 'opacity-0 group-hover:opacity-100'}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
const isCurrentlyViewing = isActive
|
|
||||||
|
|
||||||
db.channels.where("id").equals(channel.id).modify((channel) => {
|
|
||||||
channel.isOpen = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Navigate away if we're closing the currently viewed channel
|
|
||||||
if (isCurrentlyViewing) {
|
|
||||||
console.log("Navigating away from channel")
|
|
||||||
handleNavigation("/")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Close DM"
|
|
||||||
>
|
|
||||||
<XIcon className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
|
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
|
||||||
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 mb-3">
|
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-muted/50 mb-3">
|
||||||
<QuestionMarkIcon size={20} className="text-muted-foreground/50" />
|
<QuestionMarkIcon size={20} className="text-muted-foreground/50" />
|
||||||
|
|
@ -247,7 +358,9 @@ export function ChannelList({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{page !== "friends" && currentChannel && (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<span className="text-sm font-medium text-muted-foreground">No channels</span>
|
<span className="text-sm font-medium text-muted-foreground">No channels</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { Plus } from "lucide-react"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { api } from "../../../../convex/_generated/api"
|
import { api } from "../../../../convex/_generated/api"
|
||||||
|
import { Doc } from "../../../../convex/betterAuth/_generated/dataModel"
|
||||||
import DMChannelContent from "../dm/DmChannelContent"
|
import DMChannelContent from "../dm/DmChannelContent"
|
||||||
import { FriendsPage } from "../friends/friends-page"
|
import { FriendsPage } from "../friends/friends-page"
|
||||||
import { Spinner } from "../spinner"
|
import { Spinner } from "../spinner"
|
||||||
|
|
@ -26,9 +27,13 @@ export interface MainContentLayoutProps {
|
||||||
emptyChannelMessage?: string
|
emptyChannelMessage?: string
|
||||||
emptyFriendsMessage?: string
|
emptyFriendsMessage?: string
|
||||||
userId: string
|
userId: string
|
||||||
dmChannelId?: string
|
routeInfo: {
|
||||||
serverId?: string
|
type: SiPher.PageTypes
|
||||||
serverChannelId?: string
|
dmChannelId?: string
|
||||||
|
serverId?: string
|
||||||
|
serverChannelId?: string
|
||||||
|
}
|
||||||
|
userNests: Doc<"nests">[] | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MainContentLayout({
|
export function MainContentLayout({
|
||||||
|
|
@ -36,13 +41,11 @@ export function MainContentLayout({
|
||||||
emptyChannelMessage,
|
emptyChannelMessage,
|
||||||
emptyFriendsMessage,
|
emptyFriendsMessage,
|
||||||
userId,
|
userId,
|
||||||
dmChannelId,
|
routeInfo,
|
||||||
serverId,
|
userNests,
|
||||||
serverChannelId,
|
|
||||||
}: MainContentLayoutProps) {
|
}: MainContentLayoutProps) {
|
||||||
const [page, setPage] = React.useState<"friends" | "support" | "dm" | "server">(
|
const { type, dmChannelId, serverId, serverChannelId } = routeInfo
|
||||||
dmChannelId ? "dm" : serverChannelId ? "server" : "friends"
|
const [page, setPage] = React.useState<SiPher.PageTypes>(type)
|
||||||
)
|
|
||||||
const [friendsPage, setFriendsPage] = React.useState<"all" | "available">("all")
|
const [friendsPage, setFriendsPage] = React.useState<"all" | "available">("all")
|
||||||
const [friendModal, setFriendModal] = React.useState(false)
|
const [friendModal, setFriendModal] = React.useState(false)
|
||||||
const [currentChannel] = React.useState<SiPher.Channel | null>(null)
|
const [currentChannel] = React.useState<SiPher.Channel | null>(null)
|
||||||
|
|
@ -59,8 +62,9 @@ export function MainContentLayout({
|
||||||
.find((channel) => channel.id === dmChannelId)
|
.find((channel) => channel.id === dmChannelId)
|
||||||
?.participants ?? []
|
?.participants ?? []
|
||||||
|
|
||||||
const getParticipantDetails: SiPher.ParticipantDetail[] | undefined = useQuery(api.auth.getParticipantDetails,
|
const getParticipantDetails: SiPher.ParticipantDetail[] | undefined = useQuery(
|
||||||
{ participantIds }
|
api.auth.getParticipantDetails,
|
||||||
|
participantIds.length > 0 && dmChannelId ? { participantIds } : "skip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Combine channel from local DB with participant details from Convex
|
// Combine channel from local DB with participant details from Convex
|
||||||
|
|
@ -86,17 +90,11 @@ export function MainContentLayout({
|
||||||
|
|
||||||
// Sync page state with route props for seamless navigation
|
// Sync page state with route props for seamless navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dmChannelId) {
|
setPage(type);
|
||||||
setPage("dm");
|
}, [type]);
|
||||||
} else if (serverChannelId) {
|
|
||||||
setPage("server");
|
|
||||||
} else {
|
|
||||||
setPage("friends");
|
|
||||||
}
|
|
||||||
}, [dmChannelId, serverChannelId]);
|
|
||||||
|
|
||||||
// Close mobile channel list when navigating to a channel
|
// Close mobile channel list when navigating to a channel
|
||||||
const handlePageChange = React.useCallback((newPage: "friends" | "support" | "dm" | "server") => {
|
const handlePageChange = React.useCallback((newPage: SiPher.PageTypes) => {
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
setMobileChannelListOpen(false);
|
setMobileChannelListOpen(false);
|
||||||
|
|
@ -126,7 +124,6 @@ export function MainContentLayout({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
currentChannel={currentChannel}
|
currentChannel={currentChannel}
|
||||||
page={page}
|
page={page}
|
||||||
|
|
@ -140,14 +137,11 @@ export function MainContentLayout({
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Content Area - Channel List + Main Content */}
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Desktop: Always visible channel list */}
|
<div className="hidden md:flex h-full">
|
||||||
<div className="hidden md:flex">
|
|
||||||
{channelListContent}
|
{channelListContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile: Sheet-based channel list - Discord-style two-panel layout */}
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Sheet open={mobileChannelListOpen} onOpenChange={setMobileChannelListOpen}>
|
<Sheet open={mobileChannelListOpen} onOpenChange={setMobileChannelListOpen}>
|
||||||
<SheetContent side="left" className="w-[calc(100%-3rem)] max-w-[340px] p-0 [&>button]:hidden">
|
<SheetContent side="left" className="w-[calc(100%-3rem)] max-w-[340px] p-0 [&>button]:hidden">
|
||||||
|
|
@ -156,42 +150,33 @@ export function MainContentLayout({
|
||||||
<SheetDescription>Navigate between channels and DMs</SheetDescription>
|
<SheetDescription>Navigate between channels and DMs</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex h-full">
|
<div className="flex h-full">
|
||||||
{/* Left Rail - Server/Home Icons (Discord-style) */}
|
|
||||||
<div className="flex flex-col items-center w-[72px] shrink-0 bg-muted/50 py-3 gap-2">
|
<div className="flex flex-col items-center w-[72px] shrink-0 bg-muted/50 py-3 gap-2">
|
||||||
{/* Home/DMs Button */}
|
|
||||||
<MobileServerIcon
|
<MobileServerIcon
|
||||||
isActive={true}
|
isActive={true}
|
||||||
isHome
|
isHome
|
||||||
label="Direct Messages"
|
|
||||||
>
|
>
|
||||||
<LogoIcon className="size-6" />
|
<LogoIcon className="size-6" />
|
||||||
</MobileServerIcon>
|
</MobileServerIcon>
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="w-8 h-0.5 rounded-full bg-border/60 my-1" />
|
<div className="w-8 h-0.5 rounded-full bg-border/60 my-1" />
|
||||||
|
|
||||||
{/* Discover */}
|
<MobileServerIcon>
|
||||||
<MobileServerIcon label="Discover">
|
|
||||||
<CompassIcon className="size-5" weight="fill" />
|
<CompassIcon className="size-5" weight="fill" />
|
||||||
</MobileServerIcon>
|
</MobileServerIcon>
|
||||||
|
|
||||||
{/* Future: Server icons will go here */}
|
{/* Future: Server icons will go here */}
|
||||||
{/* Placeholder for servers */}
|
{/* Placeholder for servers */}
|
||||||
|
|
||||||
{/* Add Server Button */}
|
<MobileServerIcon isAddButton>
|
||||||
<MobileServerIcon label="Add a Server" isAddButton>
|
|
||||||
<Plus className="size-5" />
|
<Plus className="size-5" />
|
||||||
</MobileServerIcon>
|
</MobileServerIcon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel - Channel List */}
|
|
||||||
<div className="flex-1 flex flex-col bg-background min-w-0 border-l border-border/30">
|
<div className="flex-1 flex flex-col bg-background min-w-0 border-l border-border/30">
|
||||||
{/* Panel Header */}
|
|
||||||
<div className="flex items-center px-4 h-12 shrink-0 border-b border-border/30">
|
<div className="flex items-center px-4 h-12 shrink-0 border-b border-border/30">
|
||||||
<span className="text-sm font-semibold text-foreground">Direct Messages</span>
|
<span className="text-sm font-semibold text-foreground">Direct Messages</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Channel List Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{channelListContent}
|
{channelListContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -203,33 +188,47 @@ export function MainContentLayout({
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
{page === "dm" && dmChannelId ? (
|
{page === "dm" && dmChannelId && getParticipantDetails && (
|
||||||
getParticipantDetails ? (
|
<div className="flex flex-1 min-h-0">
|
||||||
<div className="flex flex-1 min-h-0">
|
<DMChannelContent userId={userId} channelId={dmChannelId} participantDetails={getParticipantDetails} />
|
||||||
<DMChannelContent userId={userId} channelId={dmChannelId!} participantDetails={getParticipantDetails} />
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{page === "dm" && dmChannelId && !getParticipantDetails && (
|
||||||
|
<div className="flex flex-1 min-h-0">
|
||||||
|
<div className="flex items-center justify-center flex-1">
|
||||||
|
<Spinner className="size-4 animate-spin" />
|
||||||
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="flex flex-1 min-h-0">
|
)}
|
||||||
<div className="flex items-center justify-center flex-1">
|
|
||||||
<Spinner className="size-4 animate-spin" />
|
{page === "server" && serverChannelId && (
|
||||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : page === "server" && serverChannelId ? (
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<p className="text-sm text-muted-foreground">Server channel {serverChannelId}</p>
|
<p className="text-sm text-muted-foreground">Server channel {serverChannelId}</p>
|
||||||
</div>
|
</div>
|
||||||
) : page === "friends" ? (
|
)}
|
||||||
|
|
||||||
|
{page === "friends" && (
|
||||||
<FriendsPage
|
<FriendsPage
|
||||||
userId={userId}
|
userId={userId}
|
||||||
friendsPage={friendsPage}
|
friendsPage={friendsPage}
|
||||||
socketStatus={socketStatus}
|
socketStatus={socketStatus}
|
||||||
emptyMessage={emptyFriendsMessage}
|
emptyMessage={emptyFriendsMessage}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<SettingsPage />
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{page === "nests" && (
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-sm text-muted-foreground">Nests</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{page === "discover" && <DiscoverPage userNests={userNests ?? []} />}
|
||||||
|
|
||||||
|
{page === "global-nests" && <GlobalNestsPage />}
|
||||||
|
|
||||||
|
{page === "support" && <SettingsPage />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -242,20 +241,39 @@ export function MainContentLayout({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discord-style mobile server icon component
|
function GlobalNestsPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
|
<div className="flex items-center justify-center flex-1">
|
||||||
|
<Spinner className="size-4 animate-spin" />
|
||||||
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiscoverPage({ userNests }: { userNests: Doc<"nests">[] }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
|
<div className="flex items-center justify-center flex-1">
|
||||||
|
<Spinner className="size-4 animate-spin" />
|
||||||
|
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function MobileServerIcon({
|
function MobileServerIcon({
|
||||||
children,
|
children,
|
||||||
isActive,
|
isActive,
|
||||||
isHome,
|
isHome,
|
||||||
isAddButton,
|
isAddButton,
|
||||||
label,
|
|
||||||
onClick
|
onClick
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
isHome?: boolean
|
isHome?: boolean
|
||||||
isAddButton?: boolean
|
isAddButton?: boolean
|
||||||
label?: string
|
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import UserCard from "../user/user-card"
|
||||||
|
|
||||||
export interface PageHeaderProps {
|
export interface PageHeaderProps {
|
||||||
currentChannel: SiPher.Channel | null
|
currentChannel: SiPher.Channel | null
|
||||||
page: "friends" | "support" | "dm" | "server"
|
page: SiPher.PageTypes
|
||||||
friendsPage?: "all" | "available"
|
friendsPage?: "all" | "available"
|
||||||
onFriendsPageChange?: (page: "all" | "available") => void
|
onFriendsPageChange?: (page: "all" | "available") => void
|
||||||
onAddFriend?: () => void
|
onAddFriend?: () => void
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,58 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function ScrollArea({
|
function ScrollArea({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
return (
|
return (
|
||||||
<ScrollAreaPrimitive.Root
|
<ScrollAreaPrimitive.Root
|
||||||
data-slot="scroll-area"
|
data-slot="scroll-area"
|
||||||
className={cn("relative", className)}
|
className={cn("relative overflow-hidden", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.Viewport
|
<ScrollAreaPrimitive.Viewport
|
||||||
data-slot="scroll-area-viewport"
|
data-slot="scroll-area-viewport"
|
||||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
className="size-full rounded-[inherit] [&>div]:block!"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar />
|
<ScrollBar />
|
||||||
<ScrollAreaPrimitive.Corner />
|
<ScrollAreaPrimitive.Corner />
|
||||||
</ScrollAreaPrimitive.Root>
|
</ScrollAreaPrimitive.Root>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScrollBar({
|
function ScrollBar({
|
||||||
className,
|
className,
|
||||||
orientation = "vertical",
|
orientation = "vertical",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
return (
|
return (
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
data-slot="scroll-area-scrollbar"
|
data-slot="scroll-area-scrollbar"
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex touch-none p-px transition-colors select-none",
|
"flex touch-none p-px transition-colors select-none",
|
||||||
orientation === "vertical" &&
|
orientation === "vertical" &&
|
||||||
"h-full w-2.5 border-l border-l-transparent",
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
orientation === "horizontal" &&
|
orientation === "horizontal" &&
|
||||||
"h-2.5 flex-col border-t border-t-transparent",
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
data-slot="scroll-area-thumb"
|
data-slot="scroll-area-thumb"
|
||||||
className="bg-border relative flex-1 rounded-full"
|
className="bg-border relative flex-1 rounded-full"
|
||||||
/>
|
/>
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
export { ScrollArea, ScrollBar }
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import { loadOlm } from "@/app/auth/scripts/makeKeys";
|
import { loadOlm } from "@/app/auth/scripts/makeKeys";
|
||||||
import { decryptPassword, encryptPassword, getOrCreatePasswordEncryptionKey } from "@/lib/crypto";
|
import { decryptPassword, encryptPassword, getOrCreatePasswordEncryptionKey } from "@/lib/crypto";
|
||||||
import { db } from "@/lib/db";
|
import { db, invalidateSession, validateSessionKeys } from "@/lib/db";
|
||||||
import { checkOlmStatus, getOlmAccount, handleOlmAccountCreation, SendKeysToServerFn } from "@/lib/olm";
|
import { checkOlmStatus, clearOlmAccountCache, getOlmAccount, handleOlmAccountCreation, SendKeysToServerFn } from "@/lib/olm";
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -20,10 +20,25 @@ interface OlmContextValue {
|
||||||
getSession: (recipientId: string, recipientOlmAccount: {
|
getSession: (recipientId: string, recipientOlmAccount: {
|
||||||
identityKey: { curve25519: string; ed25519: string };
|
identityKey: { curve25519: string; ed25519: string };
|
||||||
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
keyVersion?: number;
|
||||||
}) => Promise<Olm.Session | null>;
|
}) => Promise<Olm.Session | null>;
|
||||||
createInboundSession: (senderId: string, preKeyMessage: string) => Promise<Olm.Session | null>;
|
createInboundSession: (
|
||||||
|
senderId: string,
|
||||||
|
preKeyMessage: string,
|
||||||
|
senderKeyVersion?: number,
|
||||||
|
senderIdentityKey?: { curve25519: string; ed25519: string }
|
||||||
|
) => Promise<Olm.Session | null>;
|
||||||
sessions: Map<string, Olm.Session>;
|
sessions: Map<string, Olm.Session>;
|
||||||
|
|
||||||
|
// Key synchronization
|
||||||
|
validateRecipientKeys: (
|
||||||
|
recipientId: string,
|
||||||
|
recipientOlmAccount: {
|
||||||
|
identityKey: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
|
}
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
// Password & setup
|
// Password & setup
|
||||||
password: string | null;
|
password: string | null;
|
||||||
passwordError: string | null;
|
passwordError: string | null;
|
||||||
|
|
@ -72,6 +87,8 @@ export function OlmProvider({
|
||||||
const passwordSetManuallyRef = useRef(false);
|
const passwordSetManuallyRef = useRef(false);
|
||||||
// Track if we're currently loading the OLM account (prevent duplicate loads)
|
// Track if we're currently loading the OLM account (prevent duplicate loads)
|
||||||
const isLoadingAccountRef = useRef(false);
|
const isLoadingAccountRef = useRef(false);
|
||||||
|
// Trigger to force reload of OLM account
|
||||||
|
const [reloadTrigger, setReloadTrigger] = useState(0);
|
||||||
const [, forceUpdate] = useState({});
|
const [, forceUpdate] = useState({});
|
||||||
|
|
||||||
// Initialize encryption key on mount
|
// Initialize encryption key on mount
|
||||||
|
|
@ -96,7 +113,9 @@ export function OlmProvider({
|
||||||
const saveSessionToDb = useCallback(async (
|
const saveSessionToDb = useCallback(async (
|
||||||
recipientId: string,
|
recipientId: string,
|
||||||
session: Olm.Session,
|
session: Olm.Session,
|
||||||
sessionPassword: string
|
sessionPassword: string,
|
||||||
|
recipientKeyVersion?: number,
|
||||||
|
recipientIdentityKey?: { curve25519: string; ed25519: string }
|
||||||
) => {
|
) => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
|
|
@ -106,8 +125,10 @@ export function OlmProvider({
|
||||||
pickledSession: session.pickle(sessionPassword),
|
pickledSession: session.pickle(sessionPassword),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
recipientKeyVersion,
|
||||||
|
recipientIdentityKey,
|
||||||
});
|
});
|
||||||
console.debug("[OlmContext]: Session saved to DB");
|
console.debug("[OlmContext]: Session saved to DB with key version:", recipientKeyVersion);
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
// Helper: Unpickle session from database
|
// Helper: Unpickle session from database
|
||||||
|
|
@ -217,8 +238,10 @@ export function OlmProvider({
|
||||||
const loadAccount = async () => {
|
const loadAccount = async () => {
|
||||||
isLoadingAccountRef.current = true;
|
isLoadingAccountRef.current = true;
|
||||||
try {
|
try {
|
||||||
console.debug("[OlmContext]: Loading OLM account...");
|
const forceReload = reloadTrigger > 0;
|
||||||
const account = await getOlmAccount(userId, password);
|
console.log("[OlmContext]: Loading OLM account... (trigger:", reloadTrigger, "forceReload:", forceReload, ")");
|
||||||
|
|
||||||
|
const account = await getOlmAccount(userId, password, forceReload);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
console.warn("[OlmContext]: No OLM account found");
|
console.warn("[OlmContext]: No OLM account found");
|
||||||
isLoadingAccountRef.current = false;
|
isLoadingAccountRef.current = false;
|
||||||
|
|
@ -227,7 +250,7 @@ export function OlmProvider({
|
||||||
|
|
||||||
setOlmAccount(account);
|
setOlmAccount(account);
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
console.debug("[OlmContext]: OLM account loaded successfully");
|
console.log("[OlmContext]: OLM account loaded successfully");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[OlmContext]: Failed to load OLM account:", err);
|
console.error("[OlmContext]: Failed to load OLM account:", err);
|
||||||
// Password is wrong - clear it and set error
|
// Password is wrong - clear it and set error
|
||||||
|
|
@ -239,7 +262,7 @@ export function OlmProvider({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadAccount();
|
loadAccount();
|
||||||
}, [userId, password, clearPassword]);
|
}, [userId, password, reloadTrigger, clearPassword]);
|
||||||
|
|
||||||
// Clear password error
|
// Clear password error
|
||||||
const clearPasswordError = useCallback(() => {
|
const clearPasswordError = useCallback(() => {
|
||||||
|
|
@ -279,17 +302,24 @@ export function OlmProvider({
|
||||||
if (!userId || !accountPassword.trim()) return;
|
if (!userId || !accountPassword.trim()) return;
|
||||||
|
|
||||||
setOlmStatus("creating");
|
setOlmStatus("creating");
|
||||||
|
const isRotation = olmStatus === "mismatched";
|
||||||
|
|
||||||
const success = await handleOlmAccountCreation(
|
const success = await handleOlmAccountCreation(
|
||||||
userId,
|
userId,
|
||||||
accountPassword,
|
accountPassword,
|
||||||
sendKeysToServer,
|
sendKeysToServer,
|
||||||
olmStatus === "mismatched"
|
isRotation
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
setOlmStatus("synced");
|
setOlmStatus("synced");
|
||||||
setShowOlmModal(false);
|
setShowOlmModal(false);
|
||||||
setPassword(accountPassword);
|
setPassword(accountPassword);
|
||||||
|
|
||||||
|
// Clear cache and force reload OLM account from IndexedDB after creation/rotation
|
||||||
|
console.log("[OlmContext]: Keys", isRotation ? "rotated" : "created", "- clearing cache and reloading account");
|
||||||
|
clearOlmAccountCache(userId);
|
||||||
|
setReloadTrigger(prev => prev + 1);
|
||||||
} else {
|
} else {
|
||||||
setOlmStatus("not_setup");
|
setOlmStatus("not_setup");
|
||||||
}
|
}
|
||||||
|
|
@ -301,15 +331,43 @@ export function OlmProvider({
|
||||||
recipientOlmAccount: {
|
recipientOlmAccount: {
|
||||||
identityKey: { curve25519: string; ed25519: string };
|
identityKey: { curve25519: string; ed25519: string };
|
||||||
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
oneTimeKeys: Array<{ keyId: string; publicKey: string }>;
|
||||||
|
keyVersion?: number; // Optional key version for validation
|
||||||
}
|
}
|
||||||
): Promise<Olm.Session | null> => {
|
): Promise<Olm.Session | null> => {
|
||||||
|
console.log(`[OlmContext]: getSession called for ${recipientId}`, {
|
||||||
|
hasIdentityKey: !!recipientOlmAccount.identityKey,
|
||||||
|
oneTimeKeysCount: recipientOlmAccount.oneTimeKeys.length,
|
||||||
|
keyVersion: recipientOlmAccount.keyVersion
|
||||||
|
});
|
||||||
|
|
||||||
if (!validateSessionRequirements()) {
|
if (!validateSessionRequirements()) {
|
||||||
|
console.error("[OlmContext]: Session requirements validation failed");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we already have this session in memory
|
// CRITICAL: Validate recipient's keys before using cached session
|
||||||
if (sessionsRef.current.has(recipientId)) {
|
const keyVersion = recipientOlmAccount.keyVersion || 0;
|
||||||
console.debug(`[OlmContext]: Using cached session for ${recipientId}`);
|
console.log(`[OlmContext]: Validating keys for ${recipientId}, version: ${keyVersion}`);
|
||||||
|
|
||||||
|
const isValid = await validateSessionKeys(
|
||||||
|
recipientId,
|
||||||
|
keyVersion,
|
||||||
|
recipientOlmAccount.identityKey
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[OlmContext]: Key validation result for ${recipientId}: ${isValid}`);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn(`[OlmContext]: Recipient keys changed, invalidating session for ${recipientId}`);
|
||||||
|
// Remove cached session
|
||||||
|
sessionsRef.current.delete(recipientId);
|
||||||
|
// Remove from database
|
||||||
|
await invalidateSession(userId!, recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have this session in memory (after validation)
|
||||||
|
if (sessionsRef.current.has(recipientId) && isValid) {
|
||||||
|
console.log(`[OlmContext]: Using cached session for ${recipientId}`);
|
||||||
return sessionsRef.current.get(recipientId)!;
|
return sessionsRef.current.get(recipientId)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,13 +383,13 @@ export function OlmProvider({
|
||||||
try {
|
try {
|
||||||
console.debug(`[OlmContext]: Loading/creating session for user ${recipientId}`);
|
console.debug(`[OlmContext]: Loading/creating session for user ${recipientId}`);
|
||||||
|
|
||||||
// Check if session exists in DB
|
// Check if session exists in DB (after validation cleared invalid ones)
|
||||||
const existingSession = await db.olmSessions
|
const existingSession = await db.olmSessions
|
||||||
.where("[odId+recipientId]")
|
.where("[odId+recipientId]")
|
||||||
.equals([userId!, recipientId])
|
.equals([userId!, recipientId])
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (existingSession) {
|
if (existingSession && isValid) {
|
||||||
console.debug("[OlmContext]: Found existing session in DB, unpickling...");
|
console.debug("[OlmContext]: Found existing session in DB, unpickling...");
|
||||||
const session = await unpickleSessionFromDb(recipientId, existingSession.pickledSession, password!);
|
const session = await unpickleSessionFromDb(recipientId, existingSession.pickledSession, password!);
|
||||||
|
|
||||||
|
|
@ -344,40 +402,57 @@ export function OlmProvider({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new outbound session
|
// Create new outbound session
|
||||||
console.debug("[OlmContext]: Creating new outbound session...");
|
console.log("[OlmContext]: Creating new outbound session...");
|
||||||
|
|
||||||
if (recipientOlmAccount.oneTimeKeys.length === 0) {
|
if (recipientOlmAccount.oneTimeKeys.length === 0) {
|
||||||
|
console.error("[OlmContext]: No one-time keys available for recipient");
|
||||||
throw new Error("No one-time keys available for recipient");
|
throw new Error("No one-time keys available for recipient");
|
||||||
}
|
}
|
||||||
|
|
||||||
const otk = recipientOlmAccount.oneTimeKeys[0];
|
const otk = recipientOlmAccount.oneTimeKeys[0];
|
||||||
|
console.log(`[OlmContext]: Using OTK ${otk.keyId} for session creation`);
|
||||||
|
|
||||||
const Olm: typeof import("@matrix-org/olm") = await loadOlm();
|
const Olm: typeof import("@matrix-org/olm") = await loadOlm();
|
||||||
const newSession: Olm.Session = new Olm.Session();
|
const newSession: Olm.Session = new Olm.Session();
|
||||||
|
|
||||||
|
console.log(`[OlmContext]: Creating outbound session with:`, {
|
||||||
|
recipientCurve: recipientOlmAccount.identityKey.curve25519.substring(0, 20) + '...',
|
||||||
|
otkPublicKey: otk.publicKey.substring(0, 20) + '...'
|
||||||
|
});
|
||||||
|
|
||||||
newSession.create_outbound(
|
newSession.create_outbound(
|
||||||
olmAccount!,
|
olmAccount!,
|
||||||
recipientOlmAccount.identityKey.curve25519,
|
recipientOlmAccount.identityKey.curve25519,
|
||||||
otk.publicKey
|
otk.publicKey
|
||||||
);
|
);
|
||||||
|
|
||||||
console.debug(`[OlmContext]: Created session: ${newSession.session_id()}`);
|
console.log(`[OlmContext]: Created session: ${newSession.session_id()}`);
|
||||||
|
|
||||||
// Save to DB
|
// Save to DB with key version and identity key
|
||||||
await saveSessionToDb(recipientId, newSession, password!);
|
console.log(`[OlmContext]: Saving session to DB with keyVersion: ${keyVersion}`);
|
||||||
|
await saveSessionToDb(
|
||||||
|
recipientId,
|
||||||
|
newSession,
|
||||||
|
password!,
|
||||||
|
keyVersion,
|
||||||
|
recipientOlmAccount.identityKey
|
||||||
|
);
|
||||||
|
|
||||||
// Consume the OTK from server
|
// Consume the OTK from server
|
||||||
try {
|
try {
|
||||||
|
console.log(`[OlmContext]: Consuming OTK ${otk.keyId} from server`);
|
||||||
await consumeOTK({
|
await consumeOTK({
|
||||||
userId: recipientId,
|
userId: recipientId,
|
||||||
keyId: otk.keyId,
|
keyId: otk.keyId,
|
||||||
});
|
});
|
||||||
console.debug(`[OlmContext]: Consumed OTK: ${otk.keyId}`);
|
console.log(`[OlmContext]: Successfully consumed OTK: ${otk.keyId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[OlmContext]: Failed to consume OTK:", err);
|
console.error("[OlmContext]: Failed to consume OTK:", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache it
|
// Cache it
|
||||||
cacheSession(recipientId, newSession);
|
cacheSession(recipientId, newSession);
|
||||||
|
console.log(`[OlmContext]: Session cached and ready for ${recipientId}`);
|
||||||
|
|
||||||
return newSession;
|
return newSession;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -398,7 +473,9 @@ export function OlmProvider({
|
||||||
// Create an INBOUND session from a received pre-key message
|
// Create an INBOUND session from a received pre-key message
|
||||||
const createInboundSession = useCallback(async (
|
const createInboundSession = useCallback(async (
|
||||||
senderId: string,
|
senderId: string,
|
||||||
preKeyMessage: string
|
preKeyMessage: string,
|
||||||
|
senderKeyVersion?: number,
|
||||||
|
senderIdentityKey?: { curve25519: string; ed25519: string }
|
||||||
): Promise<Olm.Session | null> => {
|
): Promise<Olm.Session | null> => {
|
||||||
console.debug("[OlmContext]: Args passed to createInboundSession", { senderId, preKeyMessage });
|
console.debug("[OlmContext]: Args passed to createInboundSession", { senderId, preKeyMessage });
|
||||||
|
|
||||||
|
|
@ -426,8 +503,14 @@ export function OlmProvider({
|
||||||
|
|
||||||
console.debug(`[OlmContext]: Created inbound session: ${newSession.session_id()}`);
|
console.debug(`[OlmContext]: Created inbound session: ${newSession.session_id()}`);
|
||||||
|
|
||||||
// Save to DB
|
// Save to DB with sender's key metadata
|
||||||
await saveSessionToDb(senderId, newSession, password!);
|
await saveSessionToDb(
|
||||||
|
senderId,
|
||||||
|
newSession,
|
||||||
|
password!,
|
||||||
|
senderKeyVersion,
|
||||||
|
senderIdentityKey
|
||||||
|
);
|
||||||
|
|
||||||
// Cache it
|
// Cache it
|
||||||
cacheSession(senderId, newSession);
|
cacheSession(senderId, newSession);
|
||||||
|
|
@ -439,6 +522,36 @@ export function OlmProvider({
|
||||||
}
|
}
|
||||||
}, [validateSessionRequirements, olmAccount, password, saveSessionToDb, cacheSession]);
|
}, [validateSessionRequirements, olmAccount, password, saveSessionToDb, cacheSession]);
|
||||||
|
|
||||||
|
// Validate recipient keys and invalidate session if keys have changed
|
||||||
|
const validateRecipientKeys = useCallback(async (
|
||||||
|
recipientId: string,
|
||||||
|
recipientOlmAccount: {
|
||||||
|
identityKey: { curve25519: string; ed25519: string };
|
||||||
|
keyVersion?: number;
|
||||||
|
}
|
||||||
|
): Promise<boolean> => {
|
||||||
|
if (!userId) return false;
|
||||||
|
|
||||||
|
const keyVersion = recipientOlmAccount.keyVersion || 0;
|
||||||
|
const isValid = await validateSessionKeys(
|
||||||
|
recipientId,
|
||||||
|
keyVersion,
|
||||||
|
recipientOlmAccount.identityKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn(`[OlmContext]: Keys changed for ${recipientId}, invalidating session`);
|
||||||
|
// Remove cached session
|
||||||
|
sessionsRef.current.delete(recipientId);
|
||||||
|
// Remove from database
|
||||||
|
await invalidateSession(userId, recipientId);
|
||||||
|
// Force re-render to update UI
|
||||||
|
forceUpdate({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
const isReady = useMemo(() => {
|
const isReady = useMemo(() => {
|
||||||
return olmAccount !== null && olmStatus === "synced";
|
return olmAccount !== null && olmStatus === "synced";
|
||||||
}, [olmAccount, olmStatus]);
|
}, [olmAccount, olmStatus]);
|
||||||
|
|
@ -450,6 +563,7 @@ export function OlmProvider({
|
||||||
getSession,
|
getSession,
|
||||||
createInboundSession,
|
createInboundSession,
|
||||||
sessions: sessionsRef.current,
|
sessions: sessionsRef.current,
|
||||||
|
validateRecipientKeys,
|
||||||
password,
|
password,
|
||||||
passwordError,
|
passwordError,
|
||||||
showOlmModal,
|
showOlmModal,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { db, getOrCreateDmChannel, incrementUnread, storeMessage } from "@/lib/db";
|
import { db, getOrCreateDmChannel, storeMessage } from "@/lib/db";
|
||||||
|
import { isChannelActive, showMessageNotification } from "@/lib/notifications";
|
||||||
import { convex } from "@/lib/providers/Convex";
|
import { convex } from "@/lib/providers/Convex";
|
||||||
import { useMutation } from "convex/react";
|
import { useMutation } from "convex/react";
|
||||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
|
@ -96,7 +97,8 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
messageType: 0 | 1,
|
messageType: 0 | 1,
|
||||||
encryptedBody: string,
|
encryptedBody: string,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
fromUserId: string
|
fromUserId: string,
|
||||||
|
senderDetails?: { name: string; image?: string }
|
||||||
) => {
|
) => {
|
||||||
// Decrypt the message
|
// Decrypt the message
|
||||||
const decryptedBody = session.decrypt(messageType, encryptedBody);
|
const decryptedBody = session.decrypt(messageType, encryptedBody);
|
||||||
|
|
@ -113,9 +115,30 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
throw new Error("Invalid message format");
|
throw new Error("Invalid message format");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store message and increment unread count
|
const channelId = validatedMessage.data.channelId;
|
||||||
await storeMessage(validatedMessage.data as SiPher.Messages.ClientEncrypted.EncryptedMessage & { to: string });
|
const isActive = isChannelActive(channelId);
|
||||||
await incrementUnread(validatedMessage.data.channelId);
|
|
||||||
|
// Store message, skip unread increment if channel is active
|
||||||
|
await storeMessage(
|
||||||
|
validatedMessage.data as SiPher.Messages.ClientEncrypted.EncryptedMessage & { to: string },
|
||||||
|
{ skipUnreadIncrement: isActive }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show browser notification if channel is not active
|
||||||
|
if (!isActive && senderDetails) {
|
||||||
|
showMessageNotification({
|
||||||
|
senderName: senderDetails.name,
|
||||||
|
senderImage: senderDetails.image,
|
||||||
|
messagePreview: validatedMessage.data.content,
|
||||||
|
channelId: channelId,
|
||||||
|
userStatus: userStatusRef.current.status, // Pass current user's status
|
||||||
|
onClick: () => {
|
||||||
|
// Could navigate to the channel here if needed
|
||||||
|
window.location.href = `/channels/me/${channelId}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.debug("[Socket]: Message stored successfully");
|
console.debug("[Socket]: Message stored successfully");
|
||||||
}, [saveSessionState]);
|
}, [saveSessionState]);
|
||||||
|
|
||||||
|
|
@ -169,7 +192,7 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch participant details
|
// Fetch participant details including OLM account with key version
|
||||||
try {
|
try {
|
||||||
const participantDetails = await convex.query(api.auth.getParticipantDetails, {
|
const participantDetails = await convex.query(api.auth.getParticipantDetails, {
|
||||||
participantIds: [fromUserId]
|
participantIds: [fromUserId]
|
||||||
|
|
@ -183,11 +206,23 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
|
|
||||||
const { type, body } = data.content;
|
const { type, body } = data.content;
|
||||||
|
|
||||||
|
// Prepare sender details for notifications
|
||||||
|
const senderDetails = {
|
||||||
|
name: fromUser.displayUsername || fromUser.username || fromUser.name,
|
||||||
|
image: fromUser.image || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 0: {
|
case 0: {
|
||||||
console.debug("[Socket]: Received inbound message from pre-key message");
|
console.debug("[Socket]: Received inbound message from pre-key message");
|
||||||
|
|
||||||
const session = await createInboundSession(fromUserId, body as string);
|
// Create inbound session with sender's key metadata
|
||||||
|
const session = await createInboundSession(
|
||||||
|
fromUserId,
|
||||||
|
body as string,
|
||||||
|
fromUser.olmAccount?.keyVersion,
|
||||||
|
fromUser.olmAccount?.identityKey
|
||||||
|
);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
console.error("[Socket]: Failed to create inbound session");
|
console.error("[Socket]: Failed to create inbound session");
|
||||||
return;
|
return;
|
||||||
|
|
@ -197,7 +232,7 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
await getOrCreateDmChannel(currentUserId, fromUser);
|
await getOrCreateDmChannel(currentUserId, fromUser);
|
||||||
|
|
||||||
// Decrypt, validate, and store using helper
|
// Decrypt, validate, and store using helper
|
||||||
await decryptAndStoreMessage(session, type, body as string, currentUserId, fromUserId);
|
await decryptAndStoreMessage(session, type, body as string, currentUserId, fromUserId, senderDetails);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 1: {
|
case 1: {
|
||||||
|
|
@ -211,7 +246,7 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt, validate, and store using helper
|
// Decrypt, validate, and store using helper
|
||||||
await decryptAndStoreMessage(session, type, body as string, currentUserId, fromUserId);
|
await decryptAndStoreMessage(session, type, body as string, currentUserId, fromUserId, senderDetails);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -222,25 +257,41 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
|
|
||||||
// Process queued messages when OLM becomes ready
|
// Process queued messages when OLM becomes ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!olmAccount || !olmIsReady || messageQueueRef.current.length === 0) return;
|
if (!olmAccount || !olmIsReady) {
|
||||||
|
if (messageQueueRef.current.length > 0) {
|
||||||
|
console.log(`[Socket - processQueue]: Waiting for OLM... ${messageQueueRef.current.length} messages queued`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`[Socket - processQueue]: OLM is now ready, processing ${messageQueueRef.current.length} queued messages`);
|
if (messageQueueRef.current.length === 0) return;
|
||||||
|
|
||||||
|
console.log(`[Socket - processQueue]: ========================================`);
|
||||||
|
console.log(`[Socket - processQueue]: OLM is now ready!`);
|
||||||
|
console.log(`[Socket - processQueue]: Processing ${messageQueueRef.current.length} queued messages`);
|
||||||
|
console.log(`[Socket - processQueue]: ========================================`);
|
||||||
|
|
||||||
const processQueue = async () => {
|
const processQueue = async () => {
|
||||||
const queue = [...messageQueueRef.current];
|
const queue = [...messageQueueRef.current];
|
||||||
messageQueueRef.current = []; // Clear queue
|
messageQueueRef.current = []; // Clear queue
|
||||||
|
|
||||||
for (const data of queue) {
|
for (let i = 0; i < queue.length; i++) {
|
||||||
console.log("[Socket - processQueue]: Processing queued message:", data);
|
console.log(`[Socket - processQueue]: Processing queued message ${i + 1}/${queue.length}`);
|
||||||
await processIncomingDM(data);
|
await processIncomingDM(queue[i]);
|
||||||
}
|
}
|
||||||
|
console.log(`[Socket - processQueue]: All queued messages processed!`);
|
||||||
};
|
};
|
||||||
|
|
||||||
processQueue();
|
processQueue();
|
||||||
}, [olmAccount, olmIsReady, processIncomingDM]);
|
}, [olmAccount, olmIsReady, processIncomingDM]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user.id) return;
|
if (!user.id) {
|
||||||
|
console.warn("[Socket]: No user ID, not connecting socket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Socket]: Initializing socket connection for user:", user.id);
|
||||||
|
|
||||||
const socket: Socket = io({
|
const socket: Socket = io({
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|
@ -301,7 +352,13 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
console.log("[Socket - connect]: Connected to socket - Authentication successful!");
|
console.log("[Socket - connect]: ========================================");
|
||||||
|
console.log("[Socket - connect]: Connected to socket server!");
|
||||||
|
console.log("[Socket - connect]: Socket ID:", socket.id);
|
||||||
|
console.log("[Socket - connect]: User ID:", user.id);
|
||||||
|
console.log("[Socket - connect]: Transport:", socket.io.engine?.transport?.name);
|
||||||
|
console.log("[Socket - connect]: ========================================");
|
||||||
|
|
||||||
setSocketStatus("connected");
|
setSocketStatus("connected");
|
||||||
updateSocketInfo({
|
updateSocketInfo({
|
||||||
connectedAt: Date.now(),
|
connectedAt: Date.now(),
|
||||||
|
|
@ -370,7 +427,11 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("dm:new", async (data: { content: { type: 0 | 1; body: unknown }, participants: string[] }) => {
|
socket.on("dm:new", async (data: { content: { type: 0 | 1; body: unknown }, participants: string[] }) => {
|
||||||
console.log("[Socket - dm:new]: New DM received:", data);
|
console.log("[Socket - dm:new]: New DM received");
|
||||||
|
console.log("[Socket - dm:new]: Message type:", data.content.type);
|
||||||
|
console.log("[Socket - dm:new]: Participants:", data.participants);
|
||||||
|
console.log("[Socket - dm:new]: OLM account ready:", !!olmAccount);
|
||||||
|
console.log("[Socket - dm:new]: User ID:", user.id);
|
||||||
|
|
||||||
// Check if OLM account is loaded
|
// Check if OLM account is loaded
|
||||||
if (!olmAccount) {
|
if (!olmAccount) {
|
||||||
|
|
@ -380,8 +441,9 @@ export function SocketProvider({ children, user, refetchUser }: SocketProviderPr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process immediately if OLM is ready
|
// Process immediately if OLM is ready
|
||||||
console.debug("[Socket]: Processing incoming DM immediately:", data);
|
console.log("[Socket]: Processing incoming DM immediately");
|
||||||
await processIncomingDM(data);
|
await processIncomingDM(data);
|
||||||
|
console.log("[Socket]: Finished processing incoming DM");
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export interface OlmSession {
|
||||||
pickledSession: string; // Serialized Olm.Session
|
pickledSession: string; // Serialized Olm.Session
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
|
recipientKeyVersion?: number; // Track recipient's key version
|
||||||
|
recipientIdentityKey?: { curve25519: string; ed25519: string }; // Track recipient's identity key
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unread count per channel */
|
/** Unread count per channel */
|
||||||
|
|
@ -121,15 +123,92 @@ export async function getChannelMessages(
|
||||||
return query.reverse().sortBy("timestamp").then((msgs) => msgs.slice(0, limit));
|
return query.reverse().sortBy("timestamp").then((msgs) => msgs.slice(0, limit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Validate session keys match recipient's current keys */
|
||||||
|
export async function validateSessionKeys(
|
||||||
|
recipientId: string,
|
||||||
|
currentKeyVersion: number,
|
||||||
|
currentIdentityKey: { curve25519: string; ed25519: string }
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.debug(`[DB] Validating session keys for ${recipientId}`, {
|
||||||
|
currentKeyVersion,
|
||||||
|
currentIdentityKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessions = await db.olmSessions
|
||||||
|
.where("recipientId")
|
||||||
|
.equals(recipientId)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
console.debug(`[DB] Found ${sessions.length} existing sessions for ${recipientId}`);
|
||||||
|
|
||||||
|
if (sessions.length === 0) {
|
||||||
|
console.debug(`[DB] No existing session - validation passes`);
|
||||||
|
return true; // No session yet, validation passes
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = sessions[0];
|
||||||
|
console.debug(`[DB] Existing session metadata:`, {
|
||||||
|
recipientKeyVersion: session.recipientKeyVersion,
|
||||||
|
recipientIdentityKey: session.recipientIdentityKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if key version has changed
|
||||||
|
if (session.recipientKeyVersion !== undefined && session.recipientKeyVersion !== currentKeyVersion) {
|
||||||
|
console.warn(`[DB] Key version mismatch for ${recipientId}: local=${session.recipientKeyVersion}, server=${currentKeyVersion}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if identity key has changed
|
||||||
|
if (session.recipientIdentityKey) {
|
||||||
|
if (session.recipientIdentityKey.curve25519 !== currentIdentityKey.curve25519 ||
|
||||||
|
session.recipientIdentityKey.ed25519 !== currentIdentityKey.ed25519) {
|
||||||
|
console.warn(`[DB] Identity key mismatch for ${recipientId}`);
|
||||||
|
console.warn(`[DB] Local curve25519: ${session.recipientIdentityKey.curve25519}`);
|
||||||
|
console.warn(`[DB] Server curve25519: ${currentIdentityKey.curve25519}`);
|
||||||
|
console.warn(`[DB] Local ed25519: ${session.recipientIdentityKey.ed25519}`);
|
||||||
|
console.warn(`[DB] Server ed25519: ${currentIdentityKey.ed25519}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`[DB] Key validation passed for ${recipientId}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invalidate and remove session for a recipient */
|
||||||
|
export async function invalidateSession(userId: string, recipientId: string): Promise<void> {
|
||||||
|
await db.olmSessions
|
||||||
|
.where("[odId+recipientId]")
|
||||||
|
.equals([userId, recipientId])
|
||||||
|
.delete();
|
||||||
|
console.log(`[DB] Invalidated session for ${recipientId}`);
|
||||||
|
}
|
||||||
|
|
||||||
/** Add a message to local storage */
|
/** Add a message to local storage */
|
||||||
export async function sendMessage(
|
export async function sendMessage(
|
||||||
message: Omit<SiPher.Messages.ClientEncrypted.EncryptedMessage, "id"> & { to: string },
|
message: Omit<SiPher.Messages.ClientEncrypted.EncryptedMessage, "id"> & { to: string },
|
||||||
olmSession: Olm.Session,
|
olmSession: Olm.Session,
|
||||||
sendMessage: (message: { type: 0 | 1; body: string }, to: string) => void,
|
sendMessage: (message: { type: 0 | 1; body: string }, to: string) => void,
|
||||||
saveSession?: { userId: string; recipientId: string; password: string }
|
saveSession?: {
|
||||||
|
userId: string;
|
||||||
|
recipientId: string;
|
||||||
|
password: string;
|
||||||
|
recipientKeyVersion?: number;
|
||||||
|
recipientIdentityKey?: { curve25519: string; ed25519: string };
|
||||||
|
}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
console.log("[DB] sendMessage called", {
|
||||||
|
channelId: message.channelId,
|
||||||
|
to: message.to,
|
||||||
|
hasSession: !!olmSession,
|
||||||
|
hasSaveSession: !!saveSession
|
||||||
|
});
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
|
console.log("[DB] Generated message ID:", id);
|
||||||
|
|
||||||
await db.messages.add({ ...message, id });
|
await db.messages.add({ ...message, id });
|
||||||
|
console.log("[DB] Message added to local DB");
|
||||||
|
|
||||||
// Update channel's lastMessageAt
|
// Update channel's lastMessageAt
|
||||||
await db.channels.where("id").equals(message.channelId).modify((channel) => {
|
await db.channels.where("id").equals(message.channelId).modify((channel) => {
|
||||||
|
|
@ -137,8 +216,10 @@ export async function sendMessage(
|
||||||
channel.times.lastMessageAt = message.timestamp;
|
channel.times.lastMessageAt = message.timestamp;
|
||||||
channel.times.updatedAt = Date.now();
|
channel.times.updatedAt = Date.now();
|
||||||
});
|
});
|
||||||
|
console.log("[DB] Channel updated with last message");
|
||||||
|
|
||||||
// Encrypt the message
|
// Encrypt the message
|
||||||
|
console.log("[DB] Encrypting message...");
|
||||||
const encrypted = olmSession.encrypt(
|
const encrypted = olmSession.encrypt(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id,
|
id,
|
||||||
|
|
@ -148,29 +229,49 @@ export async function sendMessage(
|
||||||
status: message.status,
|
status: message.status,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
} satisfies SiPher.Messages.ClientEncrypted.EncryptedMessage)
|
} satisfies SiPher.Messages.ClientEncrypted.EncryptedMessage)
|
||||||
)
|
);
|
||||||
|
console.log("[DB] Message encrypted, type:", encrypted.type);
|
||||||
|
|
||||||
// CRITICAL: Save the updated session after encrypt (ratchet has advanced)
|
// CRITICAL: Save the updated session after encrypt (ratchet has advanced)
|
||||||
if (saveSession) {
|
if (saveSession) {
|
||||||
|
console.log("[DB] Saving session state...", {
|
||||||
|
recipientKeyVersion: saveSession.recipientKeyVersion,
|
||||||
|
hasIdentityKey: !!saveSession.recipientIdentityKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateData: Partial<OlmSession> = {
|
||||||
|
pickledSession: olmSession.pickle(saveSession.password),
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update key version and identity key if provided
|
||||||
|
if (saveSession.recipientKeyVersion !== undefined) {
|
||||||
|
updateData.recipientKeyVersion = saveSession.recipientKeyVersion;
|
||||||
|
}
|
||||||
|
if (saveSession.recipientIdentityKey) {
|
||||||
|
updateData.recipientIdentityKey = saveSession.recipientIdentityKey;
|
||||||
|
}
|
||||||
|
|
||||||
await db.olmSessions
|
await db.olmSessions
|
||||||
.where("[odId+recipientId]")
|
.where("[odId+recipientId]")
|
||||||
.equals([saveSession.userId, saveSession.recipientId])
|
.equals([saveSession.userId, saveSession.recipientId])
|
||||||
.modify({
|
.modify(updateData);
|
||||||
pickledSession: olmSession.pickle(saveSession.password),
|
console.log("[DB] Session state saved after encrypt with keyVersion:", saveSession.recipientKeyVersion);
|
||||||
updatedAt: Date.now(),
|
|
||||||
});
|
|
||||||
console.debug("[DB] Session state saved after encrypt");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message using the socket
|
// Send the message using the socket
|
||||||
|
console.log("[DB] Sending message via socket to:", message.to);
|
||||||
sendMessage(encrypted, message.to);
|
sendMessage(encrypted, message.to);
|
||||||
|
console.log("[DB] Message sent via socket");
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function storeMessage(
|
export async function storeMessage(
|
||||||
message: SiPher.Messages.ClientEncrypted.EncryptedMessage
|
message: SiPher.Messages.ClientEncrypted.EncryptedMessage & { to: string },
|
||||||
& { to: string }
|
options?: {
|
||||||
|
skipUnreadIncrement?: boolean; // Skip incrementing if user is viewing the channel
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db.messages.add(message);
|
await db.messages.add(message);
|
||||||
await db.channels.where("id").equals(message.channelId).modify((channel) => {
|
await db.channels.where("id").equals(message.channelId).modify((channel) => {
|
||||||
|
|
@ -178,7 +279,11 @@ export async function storeMessage(
|
||||||
channel.times.lastMessageAt = message.timestamp;
|
channel.times.lastMessageAt = message.timestamp;
|
||||||
channel.times.updatedAt = Date.now();
|
channel.times.updatedAt = Date.now();
|
||||||
});
|
});
|
||||||
await incrementUnread(message.channelId);
|
|
||||||
|
// Only increment unread if not explicitly skipped
|
||||||
|
if (!options?.skipUnreadIncrement) {
|
||||||
|
await incrementUnread(message.channelId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Increment unread count for a channel */
|
/** Increment unread count for a channel */
|
||||||
|
|
|
||||||
135
src/lib/notifications/index.ts
Normal file
135
src/lib/notifications/index.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* Browser Notification System
|
||||||
|
* Handles both unread counts and native browser notifications
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Track which channel the user is currently viewing
|
||||||
|
let activeChannelId: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the currently active channel
|
||||||
|
* Messages in this channel won't trigger notifications or increment unread
|
||||||
|
*/
|
||||||
|
export function setActiveChannel(channelId: string | null) {
|
||||||
|
activeChannelId = channelId;
|
||||||
|
console.debug("[Notifications] Active channel set to:", channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently active channel
|
||||||
|
*/
|
||||||
|
export function getActiveChannel(): string | null {
|
||||||
|
return activeChannelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is currently viewing a specific channel
|
||||||
|
*/
|
||||||
|
export function isChannelActive(channelId: string): boolean {
|
||||||
|
return activeChannelId === channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request browser notification permission
|
||||||
|
* Should be called on user interaction (button click, etc.)
|
||||||
|
*/
|
||||||
|
export async function requestNotificationPermission(): Promise<NotificationPermission> {
|
||||||
|
if (!("Notification" in window)) {
|
||||||
|
console.warn("[Notifications] Browser doesn't support notifications");
|
||||||
|
return "denied";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission === "granted") {
|
||||||
|
return "granted";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Notification.permission !== "denied") {
|
||||||
|
const permission = await Notification.requestPermission();
|
||||||
|
console.log("[Notifications] Permission:", permission);
|
||||||
|
return permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Notification.permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if notifications are enabled
|
||||||
|
*/
|
||||||
|
export function areNotificationsEnabled(): boolean {
|
||||||
|
return "Notification" in window && Notification.permission === "granted";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a browser notification for a new message
|
||||||
|
*/
|
||||||
|
export function showMessageNotification(options: {
|
||||||
|
senderName: string;
|
||||||
|
senderImage?: string;
|
||||||
|
messagePreview: string;
|
||||||
|
channelId: string;
|
||||||
|
userStatus?: "online" | "busy" | "offline" | "away"; // Current user's status
|
||||||
|
onClick?: () => void;
|
||||||
|
}) {
|
||||||
|
// Don't show notification if user status is "busy"
|
||||||
|
if (options.userStatus === "busy") {
|
||||||
|
console.debug("[Notifications] Skipping notification - user is busy");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show notification if user is viewing this channel
|
||||||
|
if (isChannelActive(options.channelId)) {
|
||||||
|
console.debug("[Notifications] Skipping notification - channel is active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if notifications not enabled
|
||||||
|
if (!areNotificationsEnabled()) {
|
||||||
|
console.debug("[Notifications] Skipping notification - not enabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show if page is focused (user is actively using the app)
|
||||||
|
if (document.hasFocus()) {
|
||||||
|
console.debug("[Notifications] Skipping notification - page has focus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notification = new Notification(`${options.senderName}`, {
|
||||||
|
body: options.messagePreview,
|
||||||
|
icon: options.senderImage || "/default-avatar.png",
|
||||||
|
badge: "/logo.png",
|
||||||
|
tag: options.channelId, // Prevents duplicate notifications for same channel
|
||||||
|
requireInteraction: false,
|
||||||
|
silent: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.onclick = () => {
|
||||||
|
window.focus();
|
||||||
|
notification.close();
|
||||||
|
options.onClick?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-close after 5 seconds
|
||||||
|
setTimeout(() => notification.close(), 5000);
|
||||||
|
|
||||||
|
console.log("[Notifications] Browser notification shown");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Notifications] Failed to show notification:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a notification sound (optional)
|
||||||
|
*/
|
||||||
|
export function playNotificationSound() {
|
||||||
|
try {
|
||||||
|
const audio = new Audio("/notification.mp3");
|
||||||
|
audio.volume = 0.5;
|
||||||
|
audio.play().catch((err) => {
|
||||||
|
console.debug("[Notifications] Failed to play sound:", err);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.debug("[Notifications] Audio not available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,17 +20,22 @@ export type SendKeysToServerFn = (args: {
|
||||||
* Unpickle and retrieve the OLM account for a user
|
* Unpickle and retrieve the OLM account for a user
|
||||||
* @param userId - The user's ID
|
* @param userId - The user's ID
|
||||||
* @param password - The password used to pickle the account
|
* @param password - The password used to pickle the account
|
||||||
|
* @param forceReload - If true, skips cache and reloads from IndexedDB
|
||||||
* @returns Promise resolving to the unpickled Olm.Account, or null if not found
|
* @returns Promise resolving to the unpickled Olm.Account, or null if not found
|
||||||
*/
|
*/
|
||||||
export async function getOlmAccount(
|
export async function getOlmAccount(
|
||||||
userId: string,
|
userId: string,
|
||||||
password: string
|
password: string,
|
||||||
|
forceReload: boolean = false
|
||||||
): Promise<any | null> {
|
): Promise<any | null> {
|
||||||
// Check cache first
|
// Check cache first (unless forcing reload)
|
||||||
if ((window as any).olmAccountCache?.[userId]) {
|
if (!forceReload && (window as any).olmAccountCache?.[userId]) {
|
||||||
|
console.debug("[OLM] Using cached account for", userId);
|
||||||
return (window as any).olmAccountCache[userId];
|
return (window as any).olmAccountCache[userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.debug("[OLM] Loading account from IndexedDB for", userId, "forceReload:", forceReload);
|
||||||
|
|
||||||
// Get pickled account from DB
|
// Get pickled account from DB
|
||||||
const pickledData = await db.olmAccounts.get(userId);
|
const pickledData = await db.olmAccounts.get(userId);
|
||||||
if (!pickledData) return null;
|
if (!pickledData) return null;
|
||||||
|
|
@ -46,9 +51,21 @@ export async function getOlmAccount(
|
||||||
}
|
}
|
||||||
(window as any).olmAccountCache[userId] = account;
|
(window as any).olmAccountCache[userId] = account;
|
||||||
|
|
||||||
|
console.debug("[OLM] Account loaded and cached for", userId);
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached OLM account for a user
|
||||||
|
* Call this after key rotation to force reload
|
||||||
|
*/
|
||||||
|
export function clearOlmAccountCache(userId: string): void {
|
||||||
|
if ((window as any).olmAccountCache?.[userId]) {
|
||||||
|
delete (window as any).olmAccountCache[userId];
|
||||||
|
console.debug("[OLM] Cleared account cache for", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has an OLM account stored locally in IndexedDB
|
* Check if user has an OLM account stored locally in IndexedDB
|
||||||
* @param userId - The user's ID
|
* @param userId - The user's ID
|
||||||
|
|
|
||||||
112
src/lib/olm/keySync.ts
Normal file
112
src/lib/olm/keySync.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
/**
|
||||||
|
* OLM Key Synchronization Utilities
|
||||||
|
*
|
||||||
|
* This module provides utilities for managing OLM key synchronization
|
||||||
|
* to handle key rotation scenarios where users change their encryption keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { convex } from "@/lib/providers/Convex";
|
||||||
|
import { api } from "../../../convex/_generated/api";
|
||||||
|
import { invalidateSession, validateSessionKeys } from "../db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a recipient's keys have changed and invalidate session if needed
|
||||||
|
* @param userId - Current user's ID
|
||||||
|
* @param recipientId - Recipient's user ID
|
||||||
|
* @returns True if keys are valid, false if they changed
|
||||||
|
*/
|
||||||
|
export async function checkRecipientKeyStatus(
|
||||||
|
userId: string,
|
||||||
|
recipientId: string
|
||||||
|
): Promise<{
|
||||||
|
isValid: boolean;
|
||||||
|
keyVersion?: number;
|
||||||
|
updatedAt?: number;
|
||||||
|
identityKey?: { curve25519: string; ed25519: string };
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Fetch current key version from server
|
||||||
|
const keyInfo = await convex.query(api.auth.getKeyVersion, {
|
||||||
|
userId: recipientId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!keyInfo) {
|
||||||
|
return { isValid: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate against locally stored key metadata
|
||||||
|
const isValid = await validateSessionKeys(
|
||||||
|
recipientId,
|
||||||
|
keyInfo.keyVersion,
|
||||||
|
keyInfo.identityKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn(`[KeySync] Keys changed for ${recipientId}, invalidating session`);
|
||||||
|
await invalidateSession(userId, recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid,
|
||||||
|
keyVersion: keyInfo.keyVersion,
|
||||||
|
updatedAt: keyInfo.updatedAt,
|
||||||
|
identityKey: keyInfo.identityKey,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[KeySync] Failed to check recipient key status:", error);
|
||||||
|
return { isValid: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch check multiple recipients' key statuses
|
||||||
|
* @param userId - Current user's ID
|
||||||
|
* @param recipientIds - Array of recipient user IDs
|
||||||
|
* @returns Map of recipientId to validation result
|
||||||
|
*/
|
||||||
|
export async function batchCheckRecipientKeys(
|
||||||
|
userId: string,
|
||||||
|
recipientIds: string[]
|
||||||
|
): Promise<Map<string, { isValid: boolean; keyVersion?: number }>> {
|
||||||
|
const results = new Map();
|
||||||
|
|
||||||
|
// Check all recipients in parallel
|
||||||
|
await Promise.all(
|
||||||
|
recipientIds.map(async (recipientId) => {
|
||||||
|
const result = await checkRecipientKeyStatus(userId, recipientId);
|
||||||
|
results.set(recipientId, result);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a periodic key sync checker
|
||||||
|
* @param userId - Current user's ID
|
||||||
|
* @param recipientIds - Array of recipient user IDs to monitor
|
||||||
|
* @param intervalMs - Check interval in milliseconds (default: 5 minutes)
|
||||||
|
* @param onKeyChange - Callback when keys change
|
||||||
|
* @returns Cleanup function to stop the interval
|
||||||
|
*/
|
||||||
|
export function createPeriodicKeySync(
|
||||||
|
userId: string,
|
||||||
|
recipientIds: string[],
|
||||||
|
intervalMs: number = 5 * 60 * 1000, // 5 minutes default
|
||||||
|
onKeyChange?: (recipientId: string) => void
|
||||||
|
): () => void {
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
console.debug("[KeySync] Running periodic key check...");
|
||||||
|
const results = await batchCheckRecipientKeys(userId, recipientIds);
|
||||||
|
|
||||||
|
results.forEach((result, recipientId) => {
|
||||||
|
if (!result.isValid) {
|
||||||
|
console.warn(`[KeySync] Keys changed for ${recipientId}`);
|
||||||
|
onKeyChange?.(recipientId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, intervalMs);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
@ -74,7 +74,7 @@ const dmEvent: SiPher.EventsType = {
|
||||||
|
|
||||||
io.to(to).emit("dm:new", dmData);
|
io.to(to).emit("dm:new", dmData);
|
||||||
|
|
||||||
console.log(`[DM] ${sender.id} → ${to} in room ${roomId}`);
|
console.log(`[DM] ${sender.id} → ${to} in room ${roomId}: ${message.body}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,15 @@ interface SocketManagerOptions {
|
||||||
authMethod?: "session" | "ott";
|
authMethod?: "session" | "ott";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RECONCILE_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
export default class SocketManager {
|
export default class SocketManager {
|
||||||
|
|
||||||
private socketIo: SocketIOServer | null = null;
|
private socketIo: SocketIOServer | null = null;
|
||||||
private events: Map<string, SiPher.EventsType[]> = new Map();
|
private events: Map<string, SiPher.EventsType[]> = new Map();
|
||||||
private options: SocketManagerOptions;
|
private options: SocketManagerOptions;
|
||||||
private convex: ConvexHttpClient;
|
private convex: ConvexHttpClient;
|
||||||
|
private reconcileTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
constructor(nextServer: HTTPServer, options: SocketManagerOptions = {}) {
|
constructor(nextServer: HTTPServer, options: SocketManagerOptions = {}) {
|
||||||
if (!nextServer) {
|
if (!nextServer) {
|
||||||
|
|
@ -148,6 +151,48 @@ export default class SocketManager {
|
||||||
return this.socketIo?.sockets.sockets.get(userId);
|
return this.socketIo?.sockets.sockets.get(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractJwt(socket: Socket): string | null {
|
||||||
|
const cookies = socket.handshake.headers.cookie;
|
||||||
|
if (!cookies || !cookies.includes("better-auth.convex_jwt")) return null;
|
||||||
|
const token = cookies.split("better-auth.convex_jwt=")[1]?.split(";")[0];
|
||||||
|
return token || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodically queries Convex for all users with a non-offline status,
|
||||||
|
* checks if they have a live socket connection, and forces offline
|
||||||
|
* any that don't.
|
||||||
|
*/
|
||||||
|
private startStatusReconciliation(): void {
|
||||||
|
if (this.reconcileTimer) return;
|
||||||
|
|
||||||
|
this.reconcileTimer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const nonOfflineUsers = await this.convex.query(api.auth.getNonOfflineUserIds, {});
|
||||||
|
if (!nonOfflineUsers || nonOfflineUsers.length === 0) return;
|
||||||
|
|
||||||
|
const connectedSocketIds = this.socketIo?.sockets.sockets;
|
||||||
|
let reconciled = 0;
|
||||||
|
|
||||||
|
for (const entry of nonOfflineUsers) {
|
||||||
|
const hasSocket = connectedSocketIds?.has(entry.userId) ?? false;
|
||||||
|
if (hasSocket) continue;
|
||||||
|
|
||||||
|
await this.convex.mutation(api.auth.forceUserOffline, { userId: entry.userId });
|
||||||
|
reconciled++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reconciled > 0) {
|
||||||
|
console.log(`[SocketManager] Reconciled ${reconciled} ghost user(s) to offline`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[SocketManager] Status reconciliation error:", error);
|
||||||
|
}
|
||||||
|
}, RECONCILE_INTERVAL_MS);
|
||||||
|
|
||||||
|
console.log(`[SocketManager] Status reconciliation started (every ${RECONCILE_INTERVAL_MS / 1000}s)`);
|
||||||
|
}
|
||||||
|
|
||||||
public async initializeEventHandler(): Promise<void> {
|
public async initializeEventHandler(): Promise<void> {
|
||||||
// Get events from the events folder
|
// Get events from the events folder
|
||||||
const socketIo = this.getSocketIo();
|
const socketIo = this.getSocketIo();
|
||||||
|
|
@ -222,21 +267,15 @@ export default class SocketManager {
|
||||||
// Handle disconnect within the connection context
|
// Handle disconnect within the connection context
|
||||||
socket.on("disconnect", async (reason) => {
|
socket.on("disconnect", async (reason) => {
|
||||||
try {
|
try {
|
||||||
const cookies = socket.handshake.headers.cookie;
|
const token = this.extractJwt(socket);
|
||||||
if (!cookies || !cookies.includes("better-auth.convex_jwt")) return;
|
if (!token) {
|
||||||
const session = cookies.split("better-auth.convex_jwt=")[1].split(";")[0];
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
console.warn(`[SocketManager] No session found for user ${socket.id}, skipping status update`);
|
console.warn(`[SocketManager] No session found for user ${socket.id}, skipping status update`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set auth token for this mutation
|
this.convex.setAuth(token);
|
||||||
this.convex.setAuth(session);
|
|
||||||
|
|
||||||
await this.convex.mutation(api.auth.updateUserStatus, {
|
await this.convex.mutation(api.auth.updateUserStatus, {
|
||||||
status: "offline",
|
status: "offline",
|
||||||
isUserSet: false,
|
|
||||||
});
|
});
|
||||||
console.log(`[SocketManager] Set user ${socket.id} status to offline`);
|
console.log(`[SocketManager] Set user ${socket.id} status to offline`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -244,5 +283,7 @@ export default class SocketManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.startStatusReconciliation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { createServer } from 'http'
|
import { config } from "dotenv";
|
||||||
import next from 'next'
|
import { createServer } from 'http';
|
||||||
import { parse } from 'url'
|
import next from 'next';
|
||||||
import SocketManager from "./lib/sockets"
|
import { parse } from 'url';
|
||||||
|
import SocketManager from "./lib/sockets";
|
||||||
|
config({ path: '.env.local' });
|
||||||
|
|
||||||
const port = parseInt(process.env.PORT || '3000', 10)
|
const port = parseInt(process.env.PORT || '3000', 10)
|
||||||
const dev = process.env.NODE_ENV !== 'production'
|
const dev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
|
||||||
3
src/types/globals.d.ts
vendored
3
src/types/globals.d.ts
vendored
|
|
@ -134,6 +134,9 @@ declare global {
|
||||||
keyId: string
|
keyId: string
|
||||||
publicKey: string
|
publicKey: string
|
||||||
}>
|
}>
|
||||||
|
createdAt: number
|
||||||
|
updatedAt: number
|
||||||
|
keyVersion: number
|
||||||
} | null
|
} | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/types/sidebar.d.ts
vendored
24
src/types/sidebar.d.ts
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace SiPher {
|
namespace SiPher {
|
||||||
|
|
||||||
|
|
@ -8,12 +9,35 @@ declare global {
|
||||||
currentChannel?: SiPher.Channel;
|
currentChannel?: SiPher.Channel;
|
||||||
disconnectSocket: () => void;
|
disconnectSocket: () => void;
|
||||||
connectSocket: () => void;
|
connectSocket: () => void;
|
||||||
|
routeInfo: RouteInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageTypes = "friends" | "support" | "dm" | "server" | "nests" | "discover" | "global-nests";
|
||||||
|
|
||||||
|
type RouteInfo = {
|
||||||
|
type: PageTypes;
|
||||||
|
} | {
|
||||||
|
type: PageTypes.dm;
|
||||||
|
dmChannelId: string;
|
||||||
|
} | {
|
||||||
|
type: PageTypes.server;
|
||||||
|
serverId: string;
|
||||||
|
serverChannelId: string;
|
||||||
|
} | {
|
||||||
|
type: PageTypes.nests;
|
||||||
|
serverId: string;
|
||||||
|
serverChannelId: string;
|
||||||
|
} | {
|
||||||
|
type: PageTypes.discover;
|
||||||
|
} | {
|
||||||
|
type: PageTypes.support;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarItem {
|
interface SidebarItem {
|
||||||
id: string;
|
id: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
|
href?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OlmStatus = "checking" | "synced" | "mismatched" | "not_setup" | "creating";
|
type OlmStatus = "checking" | "synced" | "mismatched" | "not_setup" | "creating";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue