Early Release
This project is working as expected, might have a few bugs here and there but nothing anormal.
This commit is contained in:
parent
17ce35ed6c
commit
fc8110bcad
27 changed files with 1923 additions and 469 deletions
Binary file not shown.
281
package-lock.json
generated
281
package-lock.json
generated
|
|
@ -17,6 +17,8 @@
|
||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@supabase/ssr": "^0.5.2",
|
"@supabase/ssr": "^0.5.2",
|
||||||
|
|
@ -1852,6 +1854,270 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-collection": "1.1.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.1",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
|
||||||
|
|
@ -1981,6 +2247,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-previous": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-rect": {
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@
|
||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.2",
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"@supabase/ssr": "^0.5.2",
|
"@supabase/ssr": "^0.5.2",
|
||||||
|
|
|
||||||
396
src/app/[id]/page.tsx
Normal file
396
src/app/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,396 @@
|
||||||
|
"use client"
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {AnimatePresence, motion} from 'framer-motion';
|
||||||
|
import {useTheme} from 'next-themes';
|
||||||
|
import {Button} from '@/components/ui/button';
|
||||||
|
import {Input} from '@/components/ui/input';
|
||||||
|
import {ScrollArea} from '@/components/ui/scroll-area';
|
||||||
|
import {Avatar, AvatarFallback} from '@/components/ui/avatar';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';
|
||||||
|
import {
|
||||||
|
Archive,
|
||||||
|
Ban,
|
||||||
|
Clock,
|
||||||
|
Download,
|
||||||
|
Info,
|
||||||
|
Key,
|
||||||
|
KeyRound,
|
||||||
|
MoreVertical,
|
||||||
|
Send,
|
||||||
|
ShieldCheck,
|
||||||
|
UserCheck,
|
||||||
|
UserX
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {usePathname} from "next/navigation";
|
||||||
|
import {useUser} from "@/contexts/user";
|
||||||
|
import {useToast} from "@/hooks/use-toast";
|
||||||
|
import {useSharedState} from "@/hooks/shared-states";
|
||||||
|
import {createBrowserClient} from '@/lib/supabase/browser'
|
||||||
|
import {CryptoManager} from "@/lib/crypto/keys";
|
||||||
|
import {REALTIME_SUBSCRIBE_STATES} from "@supabase/realtime-js";
|
||||||
|
|
||||||
|
export default function ChatPage() {
|
||||||
|
const {theme} = useTheme();
|
||||||
|
const {toast} = useToast();
|
||||||
|
const supabase = createBrowserClient();
|
||||||
|
|
||||||
|
const [messages, setMessages] = useState<SiPher.Thread["messages"]>([]);
|
||||||
|
const [inputMessage, setInputMessage] = useState('');
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [showKeyDialog, setShowKeyDialog] = useState(false);
|
||||||
|
const [showUserDialog, setShowUserDialog] = useState(false);
|
||||||
|
const [isEncrypted, setIsEncrypted] = useState(true);
|
||||||
|
|
||||||
|
const [realtimeSubscribed, setRealtimeSubscribed] = useState<REALTIME_SUBSCRIBE_STATES>()
|
||||||
|
|
||||||
|
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [user, setUser] = useState<SiPher.User | null>(null);
|
||||||
|
const pathName = usePathname();
|
||||||
|
const threadId = pathName.replace("/", "");
|
||||||
|
|
||||||
|
const {
|
||||||
|
user: currentUser,
|
||||||
|
getUser
|
||||||
|
} = useUser()
|
||||||
|
|
||||||
|
const {threads} = useSharedState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const channel = supabase
|
||||||
|
.channel(`messages:${threadId}`)
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{
|
||||||
|
event: '*',
|
||||||
|
schema: 'public',
|
||||||
|
table: 'messages',
|
||||||
|
},
|
||||||
|
async (payload) => {
|
||||||
|
if (payload.eventType === "INSERT") {
|
||||||
|
const messageData = payload.new as SiPher.RealtimeMessageData;
|
||||||
|
const isSender = messageData.sender_uuid === currentUser.uuid;
|
||||||
|
|
||||||
|
const decryptedMsg = await CryptoManager.decryptMessage(messageData.sender_content)
|
||||||
|
console.log(`Hello there`)
|
||||||
|
setMessages((prevState) => {
|
||||||
|
return [
|
||||||
|
...prevState,
|
||||||
|
{
|
||||||
|
id: messageData.id,
|
||||||
|
content: decryptedMsg,
|
||||||
|
sender_uuid: messageData.sender_uuid,
|
||||||
|
created_at: messageData.created_at,
|
||||||
|
isSender
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.subscribe((status) => {
|
||||||
|
setRealtimeSubscribed(status)
|
||||||
|
console.log('Realtime subscription status:', status)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
supabase.removeChannel(channel)
|
||||||
|
}
|
||||||
|
}, [threadId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getUserDataAndChat = async () => {
|
||||||
|
const {thread: getThread} = await (await fetch(`/api/user/get/thread?threadId=${threadId}`)).json() as {
|
||||||
|
thread: SiPher.Thread
|
||||||
|
};
|
||||||
|
|
||||||
|
const otherUser = getThread.participant_suuids.filter((ids) => ids !== currentUser.suuid);
|
||||||
|
const user = await getUser(`Being called from chat page (${threadId}`, otherUser[0], "suuid", true)
|
||||||
|
|
||||||
|
if (!(user.user[0].suuid && user.user[0].username)) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Could not verify the existence of this user",
|
||||||
|
variant: "destructive",
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(user.user[0])
|
||||||
|
|
||||||
|
const decryptedMsg = await CryptoManager.decryptThreadMessages(getThread["messages"], currentUser.uuid)
|
||||||
|
setMessages(decryptedMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threads.length > 0) {
|
||||||
|
setIsLoaded(true)
|
||||||
|
getUserDataAndChat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setUser(null)
|
||||||
|
setMessages([])
|
||||||
|
setIsLoaded(false)
|
||||||
|
}
|
||||||
|
}, [setUser, setMessages, setIsLoaded, threads])
|
||||||
|
|
||||||
|
if (!isLoaded || !user || realtimeSubscribed !== "SUBSCRIBED") {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
a
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock functions - replace with actual implementations
|
||||||
|
const checkUserValidity = async () => {
|
||||||
|
// Implementation for checking user validity
|
||||||
|
setShowUserDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkCurrentKey = async () => {
|
||||||
|
// Implementation for checking current key
|
||||||
|
setShowKeyDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUser = async () => {
|
||||||
|
// Implementation for deleting user
|
||||||
|
setShowDeleteDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = async (content: string) => {
|
||||||
|
if (!content.trim()) return;
|
||||||
|
setInputMessage('');
|
||||||
|
|
||||||
|
await CryptoManager.prepareAndSendMessage(
|
||||||
|
content,
|
||||||
|
currentUser.public_key,
|
||||||
|
user.public_key,
|
||||||
|
threadId
|
||||||
|
)
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-screen max-h-[900px] w-full">
|
||||||
|
{/* Chat Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarFallback>
|
||||||
|
{
|
||||||
|
user.username.charAt(0).toLocaleUpperCase()
|
||||||
|
}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">
|
||||||
|
{
|
||||||
|
user.username.charAt(0).toLocaleUpperCase() + user.username.slice(1)
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon" className="text-primary">
|
||||||
|
{isEncrypted ? <ShieldCheck className="h-5 w-5"/> : <Ban className="h-5 w-5"/>}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isEncrypted ? 'Encrypted Chat' : 'Encryption Issue'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreVertical className="h-5 w-5"/>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<DropdownMenuLabel>Chat Options</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator/>
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={checkUserValidity}>
|
||||||
|
<UserCheck className="mr-2 h-4 w-4"/>
|
||||||
|
<span>Check User</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={checkCurrentKey}>
|
||||||
|
<Key className="mr-2 h-4 w-4"/>
|
||||||
|
<span>Check Current Key</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator/>
|
||||||
|
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Clock className="mr-2 h-4 w-4"/>
|
||||||
|
<span>Message History</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Archive className="mr-2 h-4 w-4"/>
|
||||||
|
<span>Archive Chat</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<Download className="mr-2 h-4 w-4"/>
|
||||||
|
<span>Export Chat</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator/>
|
||||||
|
|
||||||
|
<DropdownMenuItem onClick={deleteUser} className="text-red-500">
|
||||||
|
<UserX className="mr-2 h-4 w-4"/>
|
||||||
|
<span>Delete User</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat Messages */}
|
||||||
|
<ScrollArea className="flex-1 p-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<AnimatePresence>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<motion.div
|
||||||
|
key={message.id}
|
||||||
|
initial={{opacity: 0, y: 20}}
|
||||||
|
animate={{opacity: 1, y: 0}}
|
||||||
|
exit={{opacity: 0}}
|
||||||
|
className={`flex ${message.isSender ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div className={`max-w-[70%] rounded-lg p-3 ${
|
||||||
|
message.isSender
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'bg-secondary'
|
||||||
|
}`}>
|
||||||
|
<p>{message.content}</p>
|
||||||
|
<div className="flex items-center justify-end space-x-1 mt-1">
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
{new Date(message.created_at).toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="p-4 border-t">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Input
|
||||||
|
value={inputMessage}
|
||||||
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
|
placeholder="Type a message..."
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage(inputMessage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => sendMessage(inputMessage)}>
|
||||||
|
<Send className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dialogs */}
|
||||||
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete User</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete this user? This will remove them from your contacts
|
||||||
|
and delete all messages. This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction className="bg-red-500">Delete</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Encryption Status</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<KeyRound className="h-4 w-4 text-green-500"/>
|
||||||
|
<span>Local private key is valid and active</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Key className="h-4 w-4 text-green-500"/>
|
||||||
|
<span>Remote public key is verified</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<ShieldCheck className="h-4 w-4 text-green-500"/>
|
||||||
|
<span>End-to-end encryption is active</span>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogAction>Close</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog open={showUserDialog} onOpenChange={setShowUserDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>User Verification</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<UserCheck className="h-4 w-4 text-green-500"/>
|
||||||
|
<span>User is verified and active</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Info className="h-4 w-4"/>
|
||||||
|
<span>Last active: 2 minutes ago</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<ShieldCheck className="h-4 w-4 text-green-500"/>
|
||||||
|
<span>Secure connection established</span>
|
||||||
|
</div>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogAction>Close</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,17 +4,32 @@ import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
||||||
|
|
||||||
// Helper function to get user data by UUID
|
// Helper function to get user data by UUID
|
||||||
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
const {searchParams} = new URL(request.url);
|
const {searchParams} = new URL(request.url);
|
||||||
const uuid = searchParams.get('uuid');
|
const uuid = searchParams.get('uuid');
|
||||||
|
const suuid = searchParams.get('suuid');
|
||||||
|
const getDetails = searchParams.get("detailed")
|
||||||
|
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
// Get specific user by UUID
|
// Get specific user by UUID
|
||||||
const userData = await getUserByUUID(supabase, uuid);
|
const userData = await getUserByUUID(supabase, uuid);
|
||||||
return NextResponse.json({user: userData});
|
return NextResponse.json({user: userData});
|
||||||
|
} else if (suuid) {
|
||||||
|
const {data, error} = await supabase.rpc('search_users', {
|
||||||
|
search_term: suuid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return NextResponse.json({error: error}, {status: 500});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getDetails) {
|
||||||
|
return NextResponse.json({user: data})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200});
|
||||||
} else {
|
} else {
|
||||||
// Get current authenticated user
|
// Get current authenticated user
|
||||||
const {data: {user}, error: authError} = await supabase.auth.getUser();
|
const {data: {user}, error: authError} = await supabase.auth.getUser();
|
||||||
|
|
@ -28,6 +43,13 @@ export async function GET(request: Request) {
|
||||||
return NextResponse.json({user: userData});
|
return NextResponse.json({user: userData});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (typeof error === "object") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: `Failed to fetch user: ${JSON.stringify(error)}`},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: `Failed to fetch user: ${error}`},
|
{error: `Failed to fetch user: ${error}`},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {createClient} from "@/lib/supabase/server";
|
import {createClient} from "@/lib/supabase/server";
|
||||||
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
||||||
import updateUserRequests from "@/lib/api/helpers/updateUserRequests";
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const {participant} = await req.json();
|
const {participant} = await req.json();
|
||||||
|
|
|
||||||
53
src/app/api/user/get/thread/route.ts
Normal file
53
src/app/api/user/get/thread/route.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import {createClient} from "@/lib/supabase/server";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const {searchParams} = new URL(request.url);
|
||||||
|
const threadId = searchParams.get('threadId');
|
||||||
|
|
||||||
|
if (!threadId) {
|
||||||
|
return NextResponse.json({
|
||||||
|
error: "No thread id provided"
|
||||||
|
}, {status: 400})
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
const {data: {user}, error: userError} = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
if (userError) {
|
||||||
|
NextResponse.json(
|
||||||
|
{error: userError},
|
||||||
|
{status: userError?.status}
|
||||||
|
)
|
||||||
|
} else if (!user) {
|
||||||
|
NextResponse.json(
|
||||||
|
{error: "User not found"},
|
||||||
|
{status: 401}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data, error} = await supabase.rpc(
|
||||||
|
"get_thread",
|
||||||
|
{
|
||||||
|
thread_uuid: threadId,
|
||||||
|
user_id: user!.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return NextResponse.json({error}, {status: 400})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({thread: data[0]}, {status: 200});
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log(e)
|
||||||
|
if (typeof e === "object") {
|
||||||
|
return NextResponse.json({error: JSON.stringify(e)}, {status: 500})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({error: e}, {status: 500})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ export async function GET(request: Request) {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
const {searchParams} = new URL(request.url);
|
const {searchParams} = new URL(request.url);
|
||||||
const uuid = searchParams.get('uuid');
|
const uuid = searchParams.get('uuid');
|
||||||
console.log('Searching for UUID:', uuid);
|
const getDetails = searchParams.get("detailed")
|
||||||
|
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
return NextResponse.json({error: "Missing UUID from request"}, {status: 400})
|
return NextResponse.json({error: "Missing UUID from request"}, {status: 400})
|
||||||
|
|
@ -36,6 +36,10 @@ export async function GET(request: Request) {
|
||||||
return NextResponse.json({error: error}, {status: 500});
|
return NextResponse.json({error: error}, {status: 500});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getDetails) {
|
||||||
|
return NextResponse.json({user: data})
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200});
|
return NextResponse.json({exists: !!(data[0].suuid && data[0].username)}, {status: 200});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import {createClient} from "@/lib/supabase/server";
|
||||||
|
import {NextResponse} from "next/server";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const {threadId, senderContent, recipientContent} = await request.json();
|
||||||
|
const supabase = await createClient();
|
||||||
|
|
||||||
|
const {data, error} = await supabase.rpc('send_message', {
|
||||||
|
thread_uuid: threadId,
|
||||||
|
sender_content: senderContent,
|
||||||
|
recipient_content: recipientContent
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({messageId: data});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (typeof error === "object") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: 'Failed to send message', details: error.message},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import {createClient} from "@/lib/supabase/server";
|
import {createClient} from "@/lib/supabase/server";
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {SupabaseClient} from "@supabase/supabase-js";
|
|
||||||
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
import getUserByUUID from "@/lib/api/helpers/getUserByUUID";
|
||||||
import updateUserRequests from "@/lib/api/helpers/updateUserRequests";
|
import updateUserRequests from "@/lib/api/helpers/updateUserRequests";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {SharedStateProvider} from "@/hooks/shared-states";
|
||||||
import ThemeProvider from "@/components/ui/theme-provider";
|
import ThemeProvider from "@/components/ui/theme-provider";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {Toaster} from "@/components/ui/toaster";
|
import {Toaster} from "@/components/ui/toaster";
|
||||||
import {RealtimeRequests} from "@/components/main/realtime/request";
|
|
||||||
|
|
||||||
const publicSans = Public_Sans({
|
const publicSans = Public_Sans({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export default function SiPher() {
|
||||||
|
|
||||||
/** CryptoManager Alert */
|
/** CryptoManager Alert */
|
||||||
const [privateKeyPresent, setPrivateKeyPresent] = useState(true);
|
const [privateKeyPresent, setPrivateKeyPresent] = useState(true);
|
||||||
|
const [backupPanel, setBackupPanel] = useState(false);
|
||||||
|
|
||||||
/** Consent Form states */
|
/** Consent Form states */
|
||||||
const [showConsentForm, setShowConsentForm] = useState(false);
|
const [showConsentForm, setShowConsentForm] = useState(false);
|
||||||
|
|
@ -164,8 +165,8 @@ export default function SiPher() {
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Private Key Missing</AlertDialogTitle>
|
<AlertDialogTitle>Private Key Missing</AlertDialogTitle>
|
||||||
<AlertDialogDescription className={"flex flex-col space-y-1"}>
|
<AlertDialogDescription className={"flex flex-col space-y-1"}>
|
||||||
<span>This app could not retrieve your private key, which means it's either lost, never stored or corrupted. Want to try again or insert it from a backup?</span>
|
<span>This app could not retrieve your private key, which means it's either lost, never stored or corrupted. Want to try again or insert it from a backup?</span>
|
||||||
<span>You can also regenerate it if you do not have it backed up, but this would mean that you'll loose access to all old messages.</span>
|
<span>You can also regenerate it if you do not have it backed up, but this would mean that you'll loose access to all old messages.</span>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
|
|
|
||||||
287
src/app/settings/page.tsx
Normal file
287
src/app/settings/page.tsx
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
"use client"
|
||||||
|
import {motion} from "framer-motion";
|
||||||
|
import {useTheme} from "next-themes";
|
||||||
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
|
import {Input} from "@/components/ui/input";
|
||||||
|
import {Label} from "@/components/ui/label";
|
||||||
|
import {Switch} from "@/components/ui/switch";
|
||||||
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {useUser} from "@/contexts/user";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {AlertTriangle, Copy, Download, Eye, EyeOff, Key, Lock, Save, User} from "lucide-react";
|
||||||
|
import {CryptoManager} from "@/lib/crypto/keys";
|
||||||
|
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const {theme, setTheme} = useTheme();
|
||||||
|
const {user} = useUser();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [privateKeyVisible, setPrivateKeyVisible] = useState(false);
|
||||||
|
const [privateKeyData, setPrivateKeyData] = useState<{ text: string; file: File } | null>(null);
|
||||||
|
const [backupError, setBackupError] = useState("");
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: {opacity: 0, y: 20},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
staggerChildren: 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: {opacity: 0, y: 20},
|
||||||
|
visible: {opacity: 1, y: 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="flex-1 space-y-8 p-8 pt-6"
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
variants={containerVariants}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight">Settings</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Manage your account settings and preferences
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="profile" className="space-y-6">
|
||||||
|
<TabsList className="w-full justify-start">
|
||||||
|
<TabsTrigger value="profile" className="flex items-center gap-2">
|
||||||
|
<User size={16}/>
|
||||||
|
Profile
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="privacy" className="flex items-center gap-2">
|
||||||
|
<Lock size={16}/>
|
||||||
|
Privacy
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<TabsContent value="profile" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Profile Information</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Update your profile information and settings
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username">Username</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
defaultValue={user.username}
|
||||||
|
placeholder="Your username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="suuid">Your SUUID</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="suuid"
|
||||||
|
value={user.suuid}
|
||||||
|
readOnly
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(user.suuid);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="privacy" className="space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Privacy Settings</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage your privacy and security preferences
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Message Encryption</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
End-to-end encryption is always enabled
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Key className="h-4 w-4 text-primary"/>
|
||||||
|
</div>
|
||||||
|
<Separator/>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Private Key Backup</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
View and download your private key for backup
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const data = await CryptoManager.exportPrivateKey();
|
||||||
|
if (data) {
|
||||||
|
setPrivateKeyData(data);
|
||||||
|
setBackupError("");
|
||||||
|
} else {
|
||||||
|
setBackupError("Failed to export private key");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setBackupError("Error accessing private key");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4 mr-2"/>
|
||||||
|
View Key
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const data = await CryptoManager.exportPrivateKey();
|
||||||
|
if (data) {
|
||||||
|
const url = URL.createObjectURL(data.file);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = data.file.name;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
setBackupError("");
|
||||||
|
} else {
|
||||||
|
setBackupError("Failed to download private key");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setBackupError("Error downloading private key");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2"/>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{backupError && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4"/>
|
||||||
|
<AlertTitle>Error</AlertTitle>
|
||||||
|
<AlertDescription>{backupError}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{privateKeyData && (
|
||||||
|
<Card className="mt-4 w-full">
|
||||||
|
<CardHeader className="py-3">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<CardTitle className="text-sm">Private Key</CardTitle>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(privateKeyData.text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setPrivateKeyData(null);
|
||||||
|
setPrivateKeyVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EyeOff className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="max-w-full overflow-hidden rounded-lg bg-secondary/50">
|
||||||
|
<pre className="p-4 text-xs overflow-x-auto whitespace-pre-wrap break-all">
|
||||||
|
{privateKeyData.text}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Separator/>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Allow Message Requests</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Receive message requests from other users
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch defaultChecked/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Alert>
|
||||||
|
<AlertTriangle className="h-4 w-4"/>
|
||||||
|
<AlertTitle>Private Key Management</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your private key is stored securely in your browser.
|
||||||
|
Make sure to back it up to avoid losing access to your messages.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</TabsContent>
|
||||||
|
</motion.div>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
className="flex justify-end"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="w-32"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
setLoading(true);
|
||||||
|
// Simulate saving
|
||||||
|
setTimeout(() => setLoading(false), 1000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<motion.div
|
||||||
|
animate={{rotate: 360}}
|
||||||
|
transition={{duration: 1, repeat: Infinity, ease: "linear"}}
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4 mr-2"/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
"Save Changes"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
// hooks/useRealtime.ts
|
// hooks/useRealtime.ts
|
||||||
import {useEffect} from 'react'
|
import {Dispatch, SetStateAction, useEffect} from 'react'
|
||||||
import {createBrowserClient} from '@/lib/supabase/browser'
|
import {createBrowserClient} from '@/lib/supabase/browser'
|
||||||
import {useUser} from '@/contexts/user'
|
import {useUser} from '@/contexts/user'
|
||||||
import {useToast} from '@/hooks/use-toast'
|
import {useToast} from '@/hooks/use-toast'
|
||||||
|
|
||||||
interface UseRealtimeProps {
|
interface UseRealtimeProps {
|
||||||
setThreads: React.Dispatch<React.SetStateAction<SiPher.Messages[]>>;
|
setThreads: Dispatch<SetStateAction<SiPher.Thread[]>>;
|
||||||
threads: SiPher.Messages[]
|
threads: SiPher.Thread[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRealtime({setThreads}: UseRealtimeProps) {
|
export function useRealtime({setThreads, threads}: UseRealtimeProps) {
|
||||||
const supabase = createBrowserClient();
|
const supabase = createBrowserClient();
|
||||||
const {user, updateUser} = useUser();
|
const {user, updateUser} = useUser();
|
||||||
const {toast} = useToast();
|
const {toast} = useToast();
|
||||||
|
|
@ -38,6 +38,7 @@ export function useRealtime({setThreads}: UseRealtimeProps) {
|
||||||
table: 'users',
|
table: 'users',
|
||||||
filter: `uuid=eq.${user.uuid}`,
|
filter: `uuid=eq.${user.uuid}`,
|
||||||
}, async (payload) => {
|
}, async (payload) => {
|
||||||
|
console.log(payload)
|
||||||
if (payload.eventType === "UPDATE") {
|
if (payload.eventType === "UPDATE") {
|
||||||
// This will also handle updates for the threads, but only for the user that accepted the request.
|
// This will also handle updates for the threads, but only for the user that accepted the request.
|
||||||
// Why? Because the function that creates the thread will also update the current user request field and remove
|
// Why? Because the function that creates the thread will also update the current user request field and remove
|
||||||
|
|
@ -48,6 +49,13 @@ export function useRealtime({setThreads}: UseRealtimeProps) {
|
||||||
requests: payload.new.requests
|
requests: payload.new.requests
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (payload.eventType === "DELETE") {
|
||||||
|
console.log(`Payload from delete: \n${payload}`)
|
||||||
|
updateUser({
|
||||||
|
...user,
|
||||||
|
//@ts-expect-error
|
||||||
|
requests: payload.new
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}).subscribe()
|
}).subscribe()
|
||||||
|
|
||||||
|
|
@ -56,11 +64,18 @@ export function useRealtime({setThreads}: UseRealtimeProps) {
|
||||||
.on("postgres_changes", {
|
.on("postgres_changes", {
|
||||||
event: "*",
|
event: "*",
|
||||||
schema: 'public',
|
schema: 'public',
|
||||||
// Using on this one because it's easier
|
|
||||||
table: "thread_participants",
|
table: "thread_participants",
|
||||||
filter: `user_uuid=${user.uuid}`,
|
filter: `user_uuid=eq.${user.uuid}`
|
||||||
}, async (payload) => {
|
}, async (payload) => {
|
||||||
console.log(payload)
|
if (payload.new !== payload.old) {
|
||||||
|
await fetchAndUpdateThreads();
|
||||||
|
}
|
||||||
}).subscribe()
|
}).subscribe()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
threadUpdate.unsubscribe()
|
||||||
|
userUpdate.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
}, [user?.uuid]);
|
}, [user?.uuid]);
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ import {GearIcon} from "@radix-ui/react-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRealtime} from "@/components/main/realtime/threads";
|
import {useRealtime} from "@/components/main/realtime/threads";
|
||||||
import {useUser} from "@/contexts/user";
|
import {useUser} from "@/contexts/user";
|
||||||
|
import {usePathname} from "next/navigation";
|
||||||
|
import {useSharedState} from "@/hooks/shared-states";
|
||||||
|
|
||||||
interface RightSidebarContentProps {
|
interface RightSidebarContentProps {
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
|
|
@ -20,30 +22,22 @@ export default function RightSidebarContent(
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
}: RightSidebarContentProps) {
|
}: RightSidebarContentProps) {
|
||||||
|
|
||||||
const [selectedThreads, setSelectedThreads] = useState("");
|
|
||||||
const [threadMenu, setThreadMenu] = useState<SiPher.Messages[] | []>([]);
|
|
||||||
const [pendingRequest, setPendingRequest] = useState<number>(0);
|
|
||||||
|
|
||||||
|
|
||||||
const [threads, setThreads] = useState<SiPher.Messages[]>([]);
|
|
||||||
useRealtime(
|
|
||||||
{setThreads}
|
|
||||||
);
|
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const {threads, setThreads} = useSharedState();
|
||||||
|
useRealtime({setThreads, threads});
|
||||||
|
|
||||||
const {user} = useUser();
|
const {user} = useUser();
|
||||||
const {username, suuid, requests = []} = user;
|
const {username, suuid, requests = []} = user;
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
|
||||||
// No need for separate requests state since it's in user object
|
|
||||||
const pendingRequests = requests?.length ?? 0;
|
const pendingRequests = requests?.length ?? 0;
|
||||||
|
|
||||||
// Move fetch to separate function
|
|
||||||
const fetchThreads = useCallback(async () => {
|
const fetchThreads = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const req = await fetch("/api/user/get/threads")
|
const req = await fetch("/api/user/get/threads")
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
const {threads} = await req.json() as { threads: SiPher.Messages[] | [] }
|
const {threads} = await req.json() as { threads: SiPher.Thread[] | [] }
|
||||||
setThreads(threads)
|
setThreads(threads)
|
||||||
} else {
|
} else {
|
||||||
setThreads([])
|
setThreads([])
|
||||||
|
|
@ -65,7 +59,6 @@ export default function RightSidebarContent(
|
||||||
body: JSON.stringify({participant: request}),
|
body: JSON.stringify({participant: request}),
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Optionally refresh threads after successful creation
|
|
||||||
fetchThreads();
|
fetchThreads();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -73,7 +66,6 @@ export default function RightSidebarContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`flex flex-col h-full w-[240px]`}>
|
<div className={`flex flex-col h-full w-[240px]`}>
|
||||||
|
|
@ -149,17 +141,19 @@ export default function RightSidebarContent(
|
||||||
<Separator className="my-2"/>
|
<Separator className="my-2"/>
|
||||||
{threads && threads.length > 0 ? (
|
{threads && threads.length > 0 ? (
|
||||||
threads.map((thread, index) => {
|
threads.map((thread, index) => {
|
||||||
|
// Gets the user's username instead of the SUUID to use as a recognizable user.
|
||||||
const otherUser = thread.participants.filter((user) => user !== username)[0];
|
const otherUser = thread.participants.filter((user) => user !== username)[0];
|
||||||
console.log(thread)
|
|
||||||
return (
|
return (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<Link href={`/${thread.thread_id}`} passHref>
|
<Link href={`/${thread.thread_id}`} passHref>
|
||||||
<div className="flex flex-row items-center mt-2">
|
<Button
|
||||||
|
variant={pathname.replace("/", "") === thread.thread_id ? "secondary" : "ghost"}
|
||||||
|
className={`w-full justify-start text-[17px] p-2`}>
|
||||||
<Avatar className="w-8 h-8 mr-3">
|
<Avatar className="w-8 h-8 mr-3">
|
||||||
<AvatarFallback>{otherUser.charAt(0).toUpperCase()}</AvatarFallback>
|
<AvatarFallback>{otherUser.charAt(0).toUpperCase()}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{otherUser}
|
{otherUser}
|
||||||
</div>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|
@ -175,7 +169,7 @@ export default function RightSidebarContent(
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-start text-[17px] py-2 text-primary"
|
className="w-full justify-start text-[17px] py-2 text-primary"
|
||||||
onClick={() => window.location.href = "/config"}
|
onClick={() => window.location.href = "/settings"}
|
||||||
>
|
>
|
||||||
<GearIcon className="w-4 h-4 mr-3"/>
|
<GearIcon className="w-4 h-4 mr-3"/>
|
||||||
Settings
|
Settings
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {Button} from "@/components/ui/button"
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import MobileHeader from "@/components/main/sidebar/mobile";
|
import MobileHeader from "@/components/main/sidebar/mobile";
|
||||||
import {useRefs, useUIState} from "@/hooks/shared-states";
|
import {useRefs, useUIState} from "@/hooks/shared-states";
|
||||||
import {useToast} from "@/hooks/use-toast";
|
|
||||||
import {useTheme} from "next-themes";
|
import {useTheme} from "next-themes";
|
||||||
import RightSidebarContent from "@/components/main/sidebar/rightsidebar";
|
import RightSidebarContent from "@/components/main/sidebar/rightsidebar";
|
||||||
|
|
||||||
|
|
@ -21,7 +20,6 @@ function Sidebar(
|
||||||
}: SidebarProps
|
}: SidebarProps
|
||||||
) {
|
) {
|
||||||
const {theme, systemTheme} = useTheme();
|
const {theme, systemTheme} = useTheme();
|
||||||
const {toast} = useToast();
|
|
||||||
|
|
||||||
const {isDrawerOpen, setIsDrawerOpen} = useUIState();
|
const {isDrawerOpen, setIsDrawerOpen} = useUIState();
|
||||||
const {drawerRef} = useRefs();
|
const {drawerRef} = useRefs();
|
||||||
|
|
@ -30,11 +28,6 @@ function Sidebar(
|
||||||
? systemTheme === "dark"
|
? systemTheme === "dark"
|
||||||
: theme === "dark"
|
: theme === "dark"
|
||||||
|
|
||||||
|
|
||||||
const handleAcceptRequest = async () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MobileHeader/>
|
<MobileHeader/>
|
||||||
|
|
@ -88,9 +81,10 @@ function Sidebar(
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
{
|
<div className={"max-h-[900px] w-full"}>{
|
||||||
children ?? null
|
children ?? null
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
import { ChevronDown } from "lucide-react"
|
import {ChevronDown} from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import {cn} from "@/lib/utils"
|
||||||
|
|
||||||
const Accordion = AccordionPrimitive.Root
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
const AccordionItem = React.forwardRef<
|
const AccordionItem = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AccordionPrimitive.Item
|
<AccordionPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("border-b", className)}
|
className={cn("border-b", className)}
|
||||||
|
|
@ -23,7 +23,7 @@ AccordionItem.displayName = "AccordionItem"
|
||||||
const AccordionTrigger = React.forwardRef<
|
const AccordionTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({className, children, ...props}, ref) => (
|
||||||
<AccordionPrimitive.Header className="flex">
|
<AccordionPrimitive.Header className="flex">
|
||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -34,7 +34,7 @@ const AccordionTrigger = React.forwardRef<
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"/>
|
||||||
</AccordionPrimitive.Trigger>
|
</AccordionPrimitive.Trigger>
|
||||||
</AccordionPrimitive.Header>
|
</AccordionPrimitive.Header>
|
||||||
))
|
))
|
||||||
|
|
@ -43,7 +43,7 @@ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
const AccordionContent = React.forwardRef<
|
const AccordionContent = React.forwardRef<
|
||||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({className, children, ...props}, ref) => (
|
||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
|
@ -54,4 +54,4 @@ const AccordionContent = React.forwardRef<
|
||||||
))
|
))
|
||||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import {cn} from "@/lib/utils"
|
||||||
import { buttonVariants } from "@/components/ui/button"
|
import {buttonVariants} from "@/components/ui/button"
|
||||||
|
|
||||||
const AlertDialog = AlertDialogPrimitive.Root
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
const AlertDialogOverlay = React.forwardRef<
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
|
@ -30,9 +30,9 @@ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
const AlertDialogContent = React.forwardRef<
|
const AlertDialogContent = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AlertDialogPortal>
|
<AlertDialogPortal>
|
||||||
<AlertDialogOverlay />
|
<AlertDialogOverlay/>
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -48,7 +48,7 @@ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
const AlertDialogHeader = ({
|
const AlertDialogHeader = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col space-y-2 text-center sm:text-left",
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
|
@ -62,7 +62,7 @@ AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
const AlertDialogFooter = ({
|
const AlertDialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
|
@ -76,7 +76,7 @@ AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
const AlertDialogTitle = React.forwardRef<
|
const AlertDialogTitle = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AlertDialogPrimitive.Title
|
<AlertDialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-lg font-semibold", className)}
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
|
@ -88,7 +88,7 @@ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
const AlertDialogDescription = React.forwardRef<
|
const AlertDialogDescription = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
|
@ -101,7 +101,7 @@ AlertDialogDescription.displayName =
|
||||||
const AlertDialogAction = React.forwardRef<
|
const AlertDialogAction = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AlertDialogPrimitive.Action
|
<AlertDialogPrimitive.Action
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(buttonVariants(), className)}
|
className={cn(buttonVariants(), className)}
|
||||||
|
|
@ -113,11 +113,11 @@ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
const AlertDialogCancel = React.forwardRef<
|
const AlertDialogCancel = React.forwardRef<
|
||||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "outline" }),
|
buttonVariants({variant: "outline"}),
|
||||||
"mt-2 sm:mt-0",
|
"mt-2 sm:mt-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
59
src/components/ui/alert.tsx
Normal file
59
src/components/ui/alert.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {cva, type VariantProps} from "class-variance-authority"
|
||||||
|
|
||||||
|
import {cn} from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Alert = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||||
|
>(({className, variant, ...props}, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({variant}), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Alert.displayName = "Alert"
|
||||||
|
|
||||||
|
const AlertTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({className, ...props}, ref) => (
|
||||||
|
<h5
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertTitle.displayName = "AlertTitle"
|
||||||
|
|
||||||
|
const AlertDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({className, ...props}, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDescription.displayName = "AlertDescription"
|
||||||
|
|
||||||
|
export {Alert, AlertTitle, AlertDescription}
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
import {Check, ChevronRight, Circle} from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import {cn} from "@/lib/utils"
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
|
@ -22,8 +22,8 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({className, inset, children, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -34,7 +34,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto" />
|
<ChevronRight className="ml-auto"/>
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
))
|
))
|
||||||
DropdownMenuSubTrigger.displayName =
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
|
@ -43,7 +43,7 @@ DropdownMenuSubTrigger.displayName =
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -59,7 +59,7 @@ DropdownMenuSubContent.displayName =
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({className, sideOffset = 4, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -79,8 +79,8 @@ const DropdownMenuItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({className, inset, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -96,7 +96,7 @@ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
>(({className, children, checked, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -108,7 +108,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4"/>
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -120,7 +120,7 @@ DropdownMenuCheckboxItem.displayName =
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({className, children, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -131,7 +131,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<Circle className="h-2 w-2 fill-current"/>
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -143,8 +143,8 @@ const DropdownMenuLabel = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({className, inset, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -160,7 +160,7 @@ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({className, ...props}, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
|
@ -172,7 +172,7 @@ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
const DropdownMenuShortcut = ({
|
const DropdownMenuShortcut = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
|
|
||||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import {cn} from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({className, ...props}, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export {Switch}
|
||||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import {cn} from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({className, ...props}, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({className, ...props}, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({className, ...props}, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export {Tabs, TabsList, TabsTrigger, TabsContent}
|
||||||
|
|
@ -23,17 +23,20 @@ export function useUser() {
|
||||||
return {
|
return {
|
||||||
user: context.user,
|
user: context.user,
|
||||||
updateUser: context.updateUser,
|
updateUser: context.updateUser,
|
||||||
getUser: async (context: string, userId?: string) => {
|
getUser: async (context: string, userId?: string, type: "suuid" | "uuid" = "uuid", detailed: boolean = false) => {
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
console.log(`useUser().getUser(): Being called by ${context}`)
|
console.log(`useUser().getUser(): Being called by ${context}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/auth/get_user?${
|
const response = await fetch(`/api/auth/get_user?${
|
||||||
userId && `uuid=${
|
userId && `${type}=${
|
||||||
encodeURIComponent(userId)
|
encodeURIComponent(userId)
|
||||||
|
}${
|
||||||
|
detailed ? "&detailed=true" : ""
|
||||||
}`
|
}`
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
if (error.message?.includes("Auth session missing!")) {
|
if (error.message?.includes("Auth session missing!")) {
|
||||||
|
|
@ -42,8 +45,7 @@ export function useUser() {
|
||||||
throw new Error(error.message || 'Authentication failed');
|
throw new Error(error.message || 'Authentication failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {user} = await response.json();
|
return await response.json();
|
||||||
return user as NonNullable<SiPher.User>;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get user:', error);
|
console.error('Failed to get user:', error);
|
||||||
router.push('/auth/login');
|
router.push('/auth/login');
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ interface SharedState {
|
||||||
// UI States
|
// UI States
|
||||||
isDrawerOpen: boolean
|
isDrawerOpen: boolean
|
||||||
setIsDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setIsDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
threads: SiPher.Thread[],
|
||||||
|
setThreads: React.Dispatch<React.SetStateAction<SiPher.Thread[]>>,
|
||||||
// Refs
|
// Refs
|
||||||
drawerRef: React.RefObject<HTMLDivElement | null>
|
drawerRef: React.RefObject<HTMLDivElement | null>
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +21,7 @@ const SharedStateContext = createContext<SharedState | undefined>(undefined)
|
||||||
export function SharedStateProvider({children}: { children: React.ReactNode }) {
|
export function SharedStateProvider({children}: { children: React.ReactNode }) {
|
||||||
// UI States
|
// UI States
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||||
|
const [threads, setThreads] = useState<SiPher.Thread[]>([]);
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const drawerRef = useRef<HTMLDivElement>(null)
|
const drawerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -30,6 +32,8 @@ export function SharedStateProvider({children}: { children: React.ReactNode }) {
|
||||||
// UI States
|
// UI States
|
||||||
isDrawerOpen,
|
isDrawerOpen,
|
||||||
setIsDrawerOpen,
|
setIsDrawerOpen,
|
||||||
|
threads,
|
||||||
|
setThreads,
|
||||||
// Refs
|
// Refs
|
||||||
drawerRef,
|
drawerRef,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default async function UpdateKey() {
|
||||||
body: JSON.stringify({publicKey: exportedPublic}),
|
body: JSON.stringify({publicKey: exportedPublic}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if(req.status !== 200) {
|
if (req.status !== 200) {
|
||||||
await CryptoManager.deletePrivateKey();
|
await CryptoManager.deletePrivateKey();
|
||||||
return {
|
return {
|
||||||
status: req.status,
|
status: req.status,
|
||||||
|
|
|
||||||
|
|
@ -18,24 +18,6 @@ export class CryptoManager {
|
||||||
private static readonly STORE_NAME = 'keys';
|
private static readonly STORE_NAME = 'keys';
|
||||||
private static readonly KEY_ID = 'private_key';
|
private static readonly KEY_ID = 'private_key';
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens db and creates the object store if needed.
|
|
||||||
* @returns {Promise<IDBDatabase>} A promise that resolves to the database instance.
|
|
||||||
*/
|
|
||||||
private static async openDB(): Promise<IDBDatabase> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const db = (event.target as IDBOpenDBRequest).result;
|
|
||||||
db.createObjectStore(this.STORE_NAME);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a fresh RSA key pair. Yay, new keys!
|
* Generates a fresh RSA key pair. Yay, new keys!
|
||||||
* @returns {Promise<CryptoKeyPair>} The generated RSA key pair.
|
* @returns {Promise<CryptoKeyPair>} The generated RSA key pair.
|
||||||
|
|
@ -135,6 +117,81 @@ export class CryptoManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async prepareAndSendMessage(
|
||||||
|
message: string,
|
||||||
|
senderPublicKey: JsonWebKey, // Our own public key
|
||||||
|
recipientPublicKey: JsonWebKey,
|
||||||
|
threadId: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Encrypt for ourselves
|
||||||
|
const senderContent = await this.encryptMessage(message, senderPublicKey);
|
||||||
|
|
||||||
|
// Encrypt for recipient
|
||||||
|
const recipientContent = await this.encryptMessage(message, recipientPublicKey);
|
||||||
|
|
||||||
|
// Send to server
|
||||||
|
const response = await fetch('/api/user/send/message', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
threadId,
|
||||||
|
senderContent,
|
||||||
|
recipientContent
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to send message');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async decryptThreadMessages(messages: any[], userUuid: string): Promise<SiPher.DecryptedMessage[]> {
|
||||||
|
try {
|
||||||
|
// Get our private key for decryption
|
||||||
|
const privateKey = await this.getPrivateKey();
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error("No private key found for decryption");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt each message
|
||||||
|
const decryptedMessages = await Promise.all(messages.map(async (message) => {
|
||||||
|
// Determine if we're the sender
|
||||||
|
const isSender = message.sender_uuid === userUuid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decryptedContent = await this.decryptMessage(message.content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
content: decryptedContent,
|
||||||
|
sender_uuid: message.sender_uuid,
|
||||||
|
created_at: message.created_at,
|
||||||
|
isSender
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt message:', message.id, error);
|
||||||
|
return {
|
||||||
|
id: message.id,
|
||||||
|
content: "Failed to decrypt message",
|
||||||
|
sender_uuid: message.sender_uuid,
|
||||||
|
created_at: message.created_at,
|
||||||
|
isSender,
|
||||||
|
error: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return decryptedMessages;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decrypting messages:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts a message using the recipient's public key.
|
* Encrypts a message using the recipient's public key.
|
||||||
* @param {string} message - The message you wanna encrypt.
|
* @param {string} message - The message you wanna encrypt.
|
||||||
|
|
@ -189,4 +246,156 @@ export class CryptoManager {
|
||||||
|
|
||||||
return new TextDecoder().decode(decrypted);
|
return new TextDecoder().decode(decrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the private key as both a downloadable file and text content.
|
||||||
|
* @param {string} filename - Name of the file to be downloaded (without extension)
|
||||||
|
* @returns {Promise<{text: string, file: File} | null>} Object containing the text content and File object, or null if no key exists
|
||||||
|
*/
|
||||||
|
static async exportPrivateKey(filename: string = 'private-key-backup'): Promise<{ text: string, file: File } | null> {
|
||||||
|
try {
|
||||||
|
const privateKey = await this.getPrivateKey();
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error("No private key found to export");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the private key to JWK format
|
||||||
|
const exportedKey = await crypto.subtle.exportKey('jwk', privateKey);
|
||||||
|
|
||||||
|
// Convert to formatted JSON string
|
||||||
|
const keyString = JSON.stringify(exportedKey, null, 2);
|
||||||
|
|
||||||
|
// Create file object
|
||||||
|
const blob = new Blob([keyString], {type: 'application/json'});
|
||||||
|
const file = new File([blob], `${filename}.json`, {type: 'application/json'});
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: keyString,
|
||||||
|
file: file
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export private key:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a provided private key matches the stored public key.
|
||||||
|
* @param {JsonWebKey} privateKeyJwk - The private key in JWK format to validate
|
||||||
|
* @param {JsonWebKey} publicKeyJwk - The public key in JWK format to validate against
|
||||||
|
* @returns {Promise<boolean>} True if the keys form a valid pair, false otherwise
|
||||||
|
*/
|
||||||
|
static async validateKeyPair(privateKeyJwk: JsonWebKey, publicKeyJwk: JsonWebKey): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Import the private key
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
privateKeyJwk,
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["decrypt"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Import the public key
|
||||||
|
const publicKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
publicKeyJwk,
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["encrypt"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a test message
|
||||||
|
const testMessage = "KeyValidationTest_" + new Date().getTime();
|
||||||
|
|
||||||
|
// Encrypt with public key
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
},
|
||||||
|
publicKey,
|
||||||
|
encoder.encode(testMessage)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Decrypt with private key
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
},
|
||||||
|
privateKey,
|
||||||
|
encrypted
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compare the result
|
||||||
|
const decryptedText = new TextDecoder().decode(decrypted);
|
||||||
|
return decryptedText === testMessage;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Key validation failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a private key from a backup after validating it against a provided public key.
|
||||||
|
* @param {JsonWebKey} privateKeyJwk - The private key in JWK format to restore
|
||||||
|
* @param {JsonWebKey} publicKeyJwk - The public key in JWK format to validate against
|
||||||
|
* @returns {Promise<boolean>} True if restoration was successful, false otherwise
|
||||||
|
*/
|
||||||
|
static async restoreFromBackup(privateKeyJwk: JsonWebKey, publicKeyJwk: JsonWebKey): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// Validate the key pair
|
||||||
|
const isValid = await this.validateKeyPair(privateKeyJwk, publicKeyJwk);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error("Invalid key pair - backup key doesn't match public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the private key
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
'jwk',
|
||||||
|
privateKeyJwk,
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["decrypt"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the validated private key
|
||||||
|
await this.storePrivateKey(privateKey);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Backup restoration failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens db and creates the object store if needed.
|
||||||
|
* @returns {Promise<IDBDatabase>} A promise that resolves to the database instance.
|
||||||
|
*/
|
||||||
|
private static async openDB(): Promise<IDBDatabase> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
db.createObjectStore(this.STORE_NAME);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/types/user.d.ts
vendored
31
src/types/user.d.ts
vendored
|
|
@ -1,15 +1,16 @@
|
||||||
import {Json} from "../../database.types";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace SiPher {
|
namespace SiPher {
|
||||||
type Messages = {
|
type Thread = {
|
||||||
thread_id: string;
|
thread_id: string;
|
||||||
participants: string[];
|
participants: string[];
|
||||||
|
participant_suuids: string[];
|
||||||
messages: {
|
messages: {
|
||||||
id: string;
|
isSender: boolean;
|
||||||
content: string;
|
id: string; // UUID
|
||||||
|
content: string; // The encrypted content (either sender_content or recipient_content)
|
||||||
|
sender_uuid: string; // UUID of sender
|
||||||
|
created_at: string; // ISO timestamp
|
||||||
}[];
|
}[];
|
||||||
indexable?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
|
|
@ -21,6 +22,24 @@ declare global {
|
||||||
username: string
|
username: string
|
||||||
uuid: string
|
uuid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DecryptedMessage {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
sender_uuid: string;
|
||||||
|
created_at: string;
|
||||||
|
isSender: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RealtimeMessageData {
|
||||||
|
created_at: string;
|
||||||
|
id: string;
|
||||||
|
recipient_content: string;
|
||||||
|
sender_content: string;
|
||||||
|
sender_uuid: string;
|
||||||
|
thread_id: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue