(feat): add live messages

(feat): update licenses
(feat): update livekit logging
(feat): add basic grid layout
(fix): remove deeplink log message
This commit is contained in:
Alois 2026-04-29 23:30:41 +02:00
commit 8e294d230e
21 changed files with 510 additions and 51 deletions

View file

@ -59,10 +59,6 @@ export default function DeeplinkProvider({
}; };
}, [isTauriEnv]); }, [isTauriEnv]);
useEffect(() => {
console.log(deeplinks);
}, [deeplinks]);
return ( return (
<deeplinkContext.Provider <deeplinkContext.Provider
value={{ value={{

View file

@ -28,6 +28,7 @@
"@tensamin/tauth": "workspace:*", "@tensamin/tauth": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"@tensamin/notifications": "workspace:*",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"comlink": "^4.4.2", "comlink": "^4.4.2",

View file

@ -25,6 +25,7 @@ import { useInitializeCall } from "@tensamin/call/store";
import { Provider as SocketContext } from "@tensamin/ttp"; import { Provider as SocketContext } from "@tensamin/ttp";
import UserContext from "@tensamin/user/context"; import UserContext from "@tensamin/user/context";
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler"; import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
import NotificationsProvider from "@tensamin/notifications/context";
import TAuthWrapper from "@tensamin/tauth/context"; import TAuthWrapper from "@tensamin/tauth/context";
@ -145,7 +146,9 @@ function AppShell() {
<CallInit /> <CallInit />
<TAuthWrapper> <TAuthWrapper>
<AppLayout> <AppLayout>
<Outlet /> <NotificationsProvider>
<Outlet />
</NotificationsProvider>
</AppLayout> </AppLayout>
</TAuthWrapper> </TAuthWrapper>
</UserContext> </UserContext>

View file

@ -7,6 +7,7 @@
"dependencies": { "dependencies": {
"@tensamin/ttp-core": "*", "@tensamin/ttp-core": "*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"sonner": "^2.0.7",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@ -54,6 +55,7 @@
"@tensamin/call": "workspace:*", "@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/notifications": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*", "@tensamin/storage": "workspace:*",
"@tensamin/tauri": "workspace:*", "@tensamin/tauri": "workspace:*",
@ -164,7 +166,11 @@
"name": "@tensamin/notifications", "name": "@tensamin/notifications",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git", "@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/ttp": "workspace:*",
"@tensamin/user": "workspace:*",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"zod": "^4.3.6", "zod": "^4.3.6",
@ -244,7 +250,7 @@
}, },
}, },
"overrides": { "overrides": {
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.12.tar.gz", "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.13.tar.gz",
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz",
}, },
"packages": { "packages": {
@ -696,7 +702,7 @@
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"],
"@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.12.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-YdwOocPnNMoE0N/yqksJM9JizkhmNv7q8sc7JMI/TOKM/0wDlQqHLqJBMd7fAGkGLCSx+DKx5z8SJbrA9YlzVg=="], "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.13.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.58.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.2", "typescript-eslint": "^8.58.1", "zod": "^4.3.6" } }, "sha512-ib5E8n9IfMzai+ucOXwTNMtu4j6m1RSHANGbFVJxP1VwJDtqIVMqpg/NtTKdRNXQYM1hK5WAScfl6r+aeyaj4g=="],
"@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-Eh/ko+4x4gF/25JyB38PNN2n/UVUIR6yxxZTpyZPx4maL00992pGmFt62kb1ejJQoooEpEfSo/ALtQDF5cjdoA=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-Eh/ko+4x4gF/25JyB38PNN2n/UVUIR6yxxZTpyZPx4maL00992pGmFt62kb1ejJQoooEpEfSo/ALtQDF5cjdoA=="],
@ -1662,8 +1668,6 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tensamin/notifications/@tensamin/ttp": ["TTP@github:Tensamin/TTP#e246d1a", {}, "Tensamin-TTP-e246d1a", "sha512-ECvnNMtGemIawk/F1FcuOPwMmNgMUaAzcRZiEwHGuG1DtjcbQ8BSF1ZG9ZpGA/9HqcnHmoreG+QGjF6uLKbi+w=="],
"@tensamin/shared/sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], "@tensamin/shared/sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="],
"@tensamin/tauri/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="], "@tensamin/tauri/lucide-react": ["lucide-react@1.8.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw=="],

View file

@ -173,11 +173,11 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/@tauri-apps_plugin-opener@2.5.3` - Folder: `licenses/@tauri-apps_plugin-opener@2.5.3`
- Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-opener` - Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-opener`
## @tensamin/ttp-core@0.0.12 ## @tensamin/ttp-core@0.0.13
- License: UNKNOWN - License: UNKNOWN
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tensamin_ttp-core@0.0.12` - Folder: `licenses/@tensamin_ttp-core@0.0.13`
- Source package dir: `packages/ttp/node_modules/@tensamin/ttp-core` - Source package dir: `packages/ttp/node_modules/@tensamin/ttp-core`
## @tensamin/ui@0.0.30 ## @tensamin/ui@0.0.30
@ -401,6 +401,26 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/react-dom@19.2.5` - Folder: `licenses/react-dom@19.2.5`
- Source package dir: `apps/tauri/node_modules/react-dom` - Source package dir: `apps/tauri/node_modules/react-dom`
## recharts@3.8.1
- License: MIT
- Homepage: https://github.com/recharts/recharts
- Repository: git+https://github.com/recharts/recharts.git
- Description: React charts
- Included files: LICENSE
- Folder: `licenses/recharts@3.8.1`
- Source package dir: `packages/call/node_modules/recharts`
## recharts@3.8.1
- License: MIT
- Homepage: https://github.com/recharts/recharts
- Repository: git+https://github.com/recharts/recharts.git
- Description: React charts
- Included files: LICENSE
- Folder: `licenses/recharts@3.8.1`
- Source package dir: `packages/call/node_modules/recharts`
## shadcn@4.2.0 ## shadcn@4.2.0
- License: MIT - License: MIT
@ -547,3 +567,13 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/zod@4.3.6` - Folder: `licenses/zod@4.3.6`
- Source package dir: `apps/web/node_modules/zod` - Source package dir: `apps/web/node_modules/zod`
## zustand@5.0.12
- License: MIT
- Homepage: https://github.com/pmndrs/zustand
- Repository: git+https://github.com/pmndrs/zustand.git
- Description: 🐻 Bear necessities for state management in React
- Included files: LICENSE
- Folder: `licenses/zustand@5.0.12`
- Source package dir: `packages/call/node_modules/zustand`

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015-present recharts
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -3,7 +3,7 @@
"specVersion": "1.5", "specVersion": "1.5",
"version": 1, "version": 1,
"metadata": { "metadata": {
"timestamp": "2026-04-28T17:49:46.479Z", "timestamp": "2026-04-29T21:21:16.348Z",
"tools": [ "tools": [
{ {
"vendor": "OpenAI", "vendor": "OpenAI",
@ -615,15 +615,15 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tensamin/ttp-core@0.0.12", "bomRef": "pkg:npm/%40tensamin/ttp-core@0.0.13",
"name": "@tensamin/ttp-core", "name": "@tensamin/ttp-core",
"version": "0.0.12", "version": "0.0.13",
"purl": "pkg:npm/%40tensamin/ttp-core@0.0.12", "purl": "pkg:npm/%40tensamin/ttp-core@0.0.13",
"externalReferences": [], "externalReferences": [],
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tensamin_ttp-core@0.0.12" "value": "licenses/@tensamin_ttp-core@0.0.13"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1395,6 +1395,76 @@
} }
] ]
}, },
{
"type": "library",
"bomRef": "pkg:npm/recharts@3.8.1",
"name": "recharts",
"version": "3.8.1",
"purl": "pkg:npm/recharts@3.8.1",
"description": "React charts",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"externalReferences": [
{
"type": "website",
"url": "https://github.com/recharts/recharts"
},
{
"type": "vcs",
"url": "git+https://github.com/recharts/recharts.git"
}
],
"properties": [
{
"name": "local:licenseFolder",
"value": "licenses/recharts@3.8.1"
},
{
"name": "local:sourcePackageDir",
"value": "packages/call/node_modules/recharts"
}
]
},
{
"type": "library",
"bomRef": "pkg:npm/recharts@3.8.1",
"name": "recharts",
"version": "3.8.1",
"purl": "pkg:npm/recharts@3.8.1",
"description": "React charts",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"externalReferences": [
{
"type": "website",
"url": "https://github.com/recharts/recharts"
},
{
"type": "vcs",
"url": "git+https://github.com/recharts/recharts.git"
}
],
"properties": [
{
"name": "local:licenseFolder",
"value": "licenses/recharts@3.8.1"
},
{
"name": "local:sourcePackageDir",
"value": "packages/call/node_modules/recharts"
}
]
},
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/shadcn@4.2.0", "bomRef": "pkg:npm/shadcn@4.2.0",
@ -1907,6 +1977,41 @@
"value": "apps/web/node_modules/zod" "value": "apps/web/node_modules/zod"
} }
] ]
},
{
"type": "library",
"bomRef": "pkg:npm/zustand@5.0.12",
"name": "zustand",
"version": "5.0.12",
"purl": "pkg:npm/zustand@5.0.12",
"description": "🐻 Bear necessities for state management in React",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"externalReferences": [
{
"type": "website",
"url": "https://github.com/pmndrs/zustand"
},
{
"type": "vcs",
"url": "git+https://github.com/pmndrs/zustand.git"
}
],
"properties": [
{
"name": "local:licenseFolder",
"value": "licenses/zustand@5.0.12"
},
{
"name": "local:sourcePackageDir",
"value": "packages/call/node_modules/zustand"
}
]
} }
] ]
} }

View file

@ -1,6 +1,6 @@
{ {
"generatedAt": "2026-04-28T17:49:46.477Z", "generatedAt": "2026-04-29T21:21:16.347Z",
"packageCount": 57, "packageCount": 60,
"packages": [ "packages": [
{ {
"name": "@codemirror/commands", "name": "@codemirror/commands",
@ -240,7 +240,7 @@
}, },
{ {
"name": "@tensamin/ttp-core", "name": "@tensamin/ttp-core",
"version": "0.0.12", "version": "0.0.13",
"license": "UNKNOWN", "license": "UNKNOWN",
"homepage": null, "homepage": null,
"repository": null, "repository": null,
@ -248,7 +248,7 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tensamin_ttp-core@0.0.12", "licenseFolder": "licenses/@tensamin_ttp-core@0.0.13",
"sourcePackageDir": "packages/ttp/node_modules/@tensamin/ttp-core" "sourcePackageDir": "packages/ttp/node_modules/@tensamin/ttp-core"
}, },
{ {
@ -549,6 +549,32 @@
"licenseFolder": "licenses/react-dom@19.2.5", "licenseFolder": "licenses/react-dom@19.2.5",
"sourcePackageDir": "apps/tauri/node_modules/react-dom" "sourcePackageDir": "apps/tauri/node_modules/react-dom"
}, },
{
"name": "recharts",
"version": "3.8.1",
"license": "MIT",
"homepage": "https://github.com/recharts/recharts",
"repository": "git+https://github.com/recharts/recharts.git",
"description": "React charts",
"files": [
"LICENSE"
],
"licenseFolder": "licenses/recharts@3.8.1",
"sourcePackageDir": "packages/call/node_modules/recharts"
},
{
"name": "recharts",
"version": "3.8.1",
"license": "MIT",
"homepage": "https://github.com/recharts/recharts",
"repository": "git+https://github.com/recharts/recharts.git",
"description": "React charts",
"files": [
"LICENSE"
],
"licenseFolder": "licenses/recharts@3.8.1",
"sourcePackageDir": "packages/call/node_modules/recharts"
},
{ {
"name": "shadcn", "name": "shadcn",
"version": "4.2.0", "version": "4.2.0",
@ -743,6 +769,19 @@
], ],
"licenseFolder": "licenses/zod@4.3.6", "licenseFolder": "licenses/zod@4.3.6",
"sourcePackageDir": "apps/web/node_modules/zod" "sourcePackageDir": "apps/web/node_modules/zod"
},
{
"name": "zustand",
"version": "5.0.12",
"license": "MIT",
"homepage": "https://github.com/pmndrs/zustand",
"repository": "git+https://github.com/pmndrs/zustand.git",
"description": "🐻 Bear necessities for state management in React",
"files": [
"LICENSE"
],
"licenseFolder": "licenses/zustand@5.0.12",
"sourcePackageDir": "packages/call/node_modules/zustand"
} }
] ]
} }

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Paul Henschel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -38,10 +38,11 @@
}, },
"overrides": { "overrides": {
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.30.tar.gz",
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.12.tar.gz" "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.13.tar.gz"
}, },
"dependencies": { "dependencies": {
"@tensamin/ttp-core": "*", "@tensamin/ttp-core": "*",
"@tensamin/ui": "*" "@tensamin/ui": "*",
"sonner": "^2.0.7"
} }
} }

View file

@ -5,14 +5,20 @@ import ScreenshareButton from "./buttons/screenshare";
import LeaveButton from "./buttons/leave"; import LeaveButton from "./buttons/leave";
export default function Actions() { export default function Actions() {
const sharedClasses = "w-14 h-10";
const sharedIconSize = 15;
return ( return (
<div className="w-full flex justify-center py-3"> <div className="w-full flex justify-center py-3">
<Card className="p-1.75"> <Card className="p-1.75">
<CardContent className="p-0! flex gap-1.75"> <CardContent className="p-0! flex gap-1.75">
<MuteButton className="w-14 h-10" iconSize={25} /> <MuteButton className={sharedClasses} iconSize={sharedIconSize} />
<DeafButton className="w-14 h-10" iconSize={25} /> <DeafButton className={sharedClasses} iconSize={sharedIconSize} />
<ScreenshareButton className="w-14 h-10" iconSize={25} /> <ScreenshareButton
<LeaveButton className="w-14 h-10" iconSize={25} /> className={sharedClasses}
iconSize={sharedIconSize}
/>
<LeaveButton className={sharedClasses} iconSize={sharedIconSize} />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -1,6 +1,6 @@
import { LeaveIcon } from "@livekit/components-react"; import { LeaveIcon } from "@livekit/components-react";
import { Button } from "@tensamin/ui"; import { Button } from "@tensamin/ui";
import { disconnect } from "../../store"; import { disconnect, useCall } from "../../store";
export default function LeaveButton({ export default function LeaveButton({
className, className,
@ -9,10 +9,13 @@ export default function LeaveButton({
className?: string; className?: string;
iconSize?: number; iconSize?: number;
}) { }) {
const state = useCall((store) => store.state);
return ( return (
<Button <Button
className={className} className={className}
onClick={() => disconnect()} onClick={() => disconnect()}
disabled={state === "closing" || state === "closed"}
variant="destructive" variant="destructive"
> >
<LeaveIcon style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }} /> <LeaveIcon style={{ scale: (iconSize ? iconSize + 100 : 100) + "%" }} />

View file

@ -113,7 +113,7 @@ export function TinyPingGraph() {
<TooltipTrigger <TooltipTrigger
render={ render={
<div <div
className="w-full h-6" className="h-6 w-24 min-w-0 shrink-0"
style={{ style={{
WebkitMaskImage: WebkitMaskImage:
"linear-gradient(to right, transparent 0%, var(--primary) 15%, var(--primary) 85%, transparent 100%)", "linear-gradient(to right, transparent 0%, var(--primary) 15%, var(--primary) 85%, transparent 100%)",
@ -122,13 +122,24 @@ export function TinyPingGraph() {
}} }}
> >
{data.length > 1 && ( {data.length > 1 && (
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer
width="100%"
height="100%"
minWidth={0}
minHeight={24}
>
<AreaChart <AreaChart
data={data} data={data}
margin={{ top: 2, right: 0, bottom: 2, left: 0 }} margin={{ top: 2, right: 0, bottom: 2, left: 0 }}
> >
<defs> <defs>
<linearGradient id="pingGradient" x1="0" y1="0" x2="0" y2="1"> <linearGradient
id="pingGradient"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop <stop
offset="0%" offset="0%"
stopColor="currentColor" stopColor="currentColor"

View file

@ -17,7 +17,6 @@ export default function TopBar() {
let active = true; let active = true;
if (userIds.length === 0) { if (userIds.length === 0) {
setUsers([]);
return () => { return () => {
active = false; active = false;
}; };
@ -42,7 +41,7 @@ export default function TopBar() {
return () => { return () => {
active = false; active = false;
}; };
}, [get, userIdsKey]); }, [get, userIdsKey, userIds]);
return ( return (
<div className="w-full flex justify-between h-12"> <div className="w-full flex justify-between h-12">

View file

@ -15,10 +15,21 @@ import {
RoomEvent, RoomEvent,
type RemoteTrack, type RemoteTrack,
Track, Track,
setLogExtension,
getLogger,
} from "livekit-client"; } from "livekit-client";
import z from "zod"; import z from "zod";
import { toast as sonnerToast } from "sonner"; import { toast as sonnerToast } from "sonner";
// logging
setLogExtension(
(level, message, context) =>
context
? log(level, "livekit", "blue", message, context)
: log(level, "livekit", "blue", message),
getLogger("tensamin"),
);
type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting"; type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting";
type CallView = "preview" | "focused" | "grid"; type CallView = "preview" | "focused" | "grid";
type CurrentCallData = type CurrentCallData =
@ -76,6 +87,7 @@ const e2eeWorker = new Worker(
const room = new Room({ const room = new Room({
dynacast: true, dynacast: true,
adaptiveStream: true, adaptiveStream: true,
loggerName: "tensamin",
encryption: { encryption: {
keyProvider, keyProvider,
worker: e2eeWorker, worker: e2eeWorker,
@ -83,6 +95,7 @@ const room = new Room({
}); });
const remoteAudioElements = new Map<string, HTMLMediaElement>(); const remoteAudioElements = new Map<string, HTMLMediaElement>();
// audio helpers
function attachRemoteAudio(trackSid: string, element: HTMLMediaElement) { function attachRemoteAudio(trackSid: string, element: HTMLMediaElement) {
const existingElement = remoteAudioElements.get(trackSid); const existingElement = remoteAudioElements.get(trackSid);
@ -113,6 +126,7 @@ function clearRemoteAudio() {
} }
} }
// utils
function requireRuntime(runtime: Runtime | null): Runtime { function requireRuntime(runtime: Runtime | null): Runtime {
if (!runtime) { if (!runtime) {
throw new Error("Call store is not initialized"); throw new Error("Call store is not initialized");
@ -132,6 +146,7 @@ export function syncParticipantState() {
}); });
} }
// set state functions
export function setCallRuntime(runtime: Runtime) { export function setCallRuntime(runtime: Runtime) {
useCall.setState({ runtime }); useCall.setState({ runtime });
} }
@ -154,6 +169,7 @@ export function setCurrentCallData(
useCall.setState({ currentCallData }); useCall.setState({ currentCallData });
} }
// more utils
export async function openCallPage(callId: string) { export async function openCallPage(callId: string) {
await requireRuntime(useCall.getState().runtime).navigate({ await requireRuntime(useCall.getState().runtime).navigate({
to: "/call", to: "/call",
@ -225,6 +241,7 @@ export function disconnect() {
}); });
room.disconnect(); room.disconnect();
useCall.setState({ state: "closed" });
syncParticipantState(); syncParticipantState();
} }
@ -594,7 +611,7 @@ export function useInitializeCall() {
}); });
}) })
.catch((err) => { .catch((err) => {
log(1, "Call", "red", "Failed to get call data", { log(1, "call", "red", "Failed to get call data", {
callId, callId,
error: err, error: err,
}); });

View file

@ -1,3 +1,173 @@
import { RoomEvent } from "livekit-client";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCall } from "../../store";
const TILE_ASPECT_RATIO = 16 / 9;
const GRID_GAP = 12;
type GridLayout = {
columns: number;
rows: number;
tileWidth: number;
tileHeight: number;
rowCounts: number[];
};
function buildBalancedRows(itemCount: number, rows: number) {
if (itemCount === 0 || rows === 0) {
return [];
}
const baseCount = Math.floor(itemCount / rows);
const remainder = itemCount % rows;
return Array.from(
{ length: rows },
(_, index) => baseCount + (index < remainder ? 1 : 0),
);
}
function calculateOptimalGridLayout(
containerWidth: number,
containerHeight: number,
itemCount: number,
): GridLayout {
if (containerWidth <= 0 || containerHeight <= 0 || itemCount <= 0) {
return {
columns: 0,
rows: 0,
tileWidth: 0,
tileHeight: 0,
rowCounts: [],
};
}
let bestLayout: GridLayout = {
columns: 1,
rows: itemCount,
tileWidth: 0,
tileHeight: 0,
rowCounts: Array.from({ length: itemCount }, () => 1),
};
for (let columns = 1; columns <= itemCount; columns++) {
const rows = Math.ceil(itemCount / columns);
const maxTileWidth =
(containerWidth - GRID_GAP * Math.max(columns - 1, 0)) / columns;
const maxTileHeight =
(containerHeight - GRID_GAP * Math.max(rows - 1, 0)) / rows;
const tileWidth = Math.min(maxTileWidth, maxTileHeight * TILE_ASPECT_RATIO);
const tileHeight = tileWidth / TILE_ASPECT_RATIO;
if (tileWidth <= 0 || tileHeight <= 0) {
continue;
}
const currentArea = tileWidth * tileHeight;
const bestArea = bestLayout.tileWidth * bestLayout.tileHeight;
if (currentArea > bestArea) {
bestLayout = {
columns,
rows,
tileWidth,
tileHeight,
rowCounts: buildBalancedRows(itemCount, rows),
};
}
}
return bestLayout;
}
export default function View() { export default function View() {
return <div className="w-full h-full bg-red-500">Grid</div>; const room = useCall((state) => state.room);
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
const [participantCount, setParticipantCount] = useState(
() => room.remoteParticipants.size + 1,
);
useEffect(() => {
const element = containerRef.current;
if (!element) {
return;
}
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setContainerSize({ width, height });
});
observer.observe(element);
return () => observer.disconnect();
}, []);
useEffect(() => {
const syncParticipantCount = () => {
setParticipantCount(room.remoteParticipants.size + 1);
};
syncParticipantCount();
room.on(RoomEvent.Connected, syncParticipantCount);
room.on(RoomEvent.Disconnected, syncParticipantCount);
room.on(RoomEvent.ParticipantConnected, syncParticipantCount);
room.on(RoomEvent.ParticipantDisconnected, syncParticipantCount);
return () => {
room.off(RoomEvent.Connected, syncParticipantCount);
room.off(RoomEvent.Disconnected, syncParticipantCount);
room.off(RoomEvent.ParticipantConnected, syncParticipantCount);
room.off(RoomEvent.ParticipantDisconnected, syncParticipantCount);
};
}, [room]);
const layout = useMemo(
() =>
calculateOptimalGridLayout(
containerSize.width,
containerSize.height,
participantCount,
),
[containerSize.height, containerSize.width, participantCount],
);
const rows = useMemo(() => {
let nextParticipant = 1;
return layout.rowCounts.map((count, rowIndex) => {
const participants = Array.from(
{ length: count },
() => nextParticipant++,
);
return { rowIndex, participants };
});
}, [layout.rowCounts]);
return (
<div ref={containerRef} className="h-full w-full overflow-hidden p-3">
<div className="flex h-full w-full flex-col items-center justify-center gap-3">
{rows.map((row) => (
<div key={row.rowIndex} className="flex justify-center gap-3">
{row.participants.map((participant) => (
<div
key={participant}
className="flex items-center justify-center rounded-xl bg-red-500"
style={{
width: layout.tileWidth,
height: layout.tileHeight,
}}
>
Test {participant}
</div>
))}
</div>
))}
</div>
</div>
);
} }

View file

@ -12,7 +12,6 @@ export default function Preview() {
let active = true; let active = true;
if (!currentCallData?.exists) { if (!currentCallData?.exists) {
setData([]);
return () => { return () => {
active = false; active = false;
}; };
@ -52,7 +51,7 @@ export default function Preview() {
})} })}
</div> </div>
) : ( ) : (
<p className="text-2xl">Invalid call</p> <p className="text-2xl">Call expired</p>
)} )}
</div> </div>
); );

View file

@ -12,7 +12,11 @@
"build": "tsc -p tsconfig.json --noEmit" "build": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git", "@tensamin/ttp": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/crypto": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/user": "workspace:*",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"zod": "^4.3.6" "zod": "^4.3.6"

View file

@ -1,14 +1,43 @@
import * as React from "react"; import { useCrypto } from "@tensamin/crypto/context";
import { useStorage } from "@tensamin/storage/context";
import { useUser } from "@tensamin/user/context";
import { useTTP } from "@tensamin/ttp";
import { createContext, useEffect, useContext } from "react";
import z from "zod";
import { toast as sonnerToast } from "sonner";
import { message as messageSchema } from "@tensamin/shared/data";
export const context = React.createContext<contextType | undefined>(undefined); export const context = createContext<contextType | undefined>(undefined);
/**
* Executes Provider.
* @param props Parameter props.
* @returns unknown.
*/
export default function Provider(props: { children: React.ReactNode }) { export default function Provider(props: { children: React.ReactNode }) {
// live_messages const { subscribePush } = useTTP();
const { load } = useStorage();
const { get } = useUser();
const { decryptText, getSharedSecret } = useCrypto();
useEffect(() => {
subscribePush(async (ttpMessage) => {
if (ttpMessage.type === "message_live") {
const { message, sender_id } = ttpMessage.data as {
message: z.infer<typeof messageSchema>;
sender_id: number;
};
const decryptedContent = await decryptText(
await getSharedSecret(
await load("private_key"),
await get(await load("user_id")).then((data) => data.public_key),
await get(sender_id).then((data) => data.public_key),
),
message.content,
);
sonnerToast(decryptedContent, {
icon: <div>A</div>,
});
}
});
}, [subscribePush, decryptText, getSharedSecret, load, get]);
return ( return (
<context.Provider value={{ test: () => {} }}> <context.Provider value={{ test: () => {} }}>
@ -27,7 +56,7 @@ type contextType = {
* @returns contextType. * @returns contextType.
*/ */
export function useNotifications(): contextType { export function useNotifications(): contextType {
const ctx = React.useContext(context); const ctx = useContext(context);
if (!ctx) { if (!ctx) {
throw new Error("useNotifications must be used within a ChatProvider"); throw new Error("useNotifications must be used within a ChatProvider");
} }

View file

@ -12,10 +12,10 @@ const fileFromMessage = z.object({
type: z.enum(["image", "image_top_right", "file"]), type: z.enum(["image", "image_top_right", "file"]),
}); });
const message = z.object({ export const message = z.object({
height: z.number(), height: z.number(),
not_encrypted: z.boolean().optional(), not_encrypted: z.boolean().optional(),
sent_by_self: z.boolean(), sent_by_self: z.boolean().optional(),
send_time: z.number(), send_time: z.number(),
content: z.base64(), content: z.base64(),
files: z.array(fileFromMessage).optional(), files: z.array(fileFromMessage).optional(),
@ -113,10 +113,9 @@ export const ttp = {
}), }),
}, },
message_live: { message_live: {
request: z.object({}), request: z.object({}).optional(),
response: z.object({ response: z.object({
sender_id: z.number(), sender_id: z.number(),
send_time: z.number(),
message, message,
}), }),
}, },