From 2a8fade42075fd168017e1e4fb0502e3fda37b90 Mon Sep 17 00:00:00 2001 From: forgejo-actions Date: Wed, 3 Jun 2026 07:26:40 +0000 Subject: [PATCH 1/9] (qol): update release flake hash --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 41b621a..c1ae625 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,8 @@ outputs = {nixpkgs, ...}: let systems = ["x86_64-linux"]; forAllSystems = nixpkgs.lib.genAttrs systems; - version = "0.0.7"; - x86_64DebHash = "sha256-gRkUEx1T7mrCgFEVp0X/ZiaGrKWzO+cCvv33DKCkB+o="; + version = "0.0.8"; + x86_64DebHash = "sha256-xC8YHVk+JpeMvXHB1dZihYLFyzFDXne4bkV6TZiOFrE="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { packages = forAllSystems (system: let From be56080ba1b289a305bbc1e3992b2cdd2b2da80a Mon Sep 17 00:00:00 2001 From: Alois Date: Wed, 3 Jun 2026 14:53:18 +0200 Subject: [PATCH 2/9] (fix): remove unused dependencies (fix): remove dead code (fix): remove unused exports --- .fallowrc.json | 7 +- apps/electron/package.json | 4 +- apps/electron/src/main/main.ts | 7 +- apps/tauri/package.json | 6 +- apps/web/package.json | 13 +--- apps/web/src/components/sidebar.tsx | 2 +- apps/web/src/features/legal/screen.tsx | 2 +- apps/web/src/features/settings/components.tsx | 70 +------------------ apps/web/src/routes/app/layout.tsx | 21 +----- .../web/src/routes/app/useShowMobileNavbar.ts | 14 ++++ bun.lock | 49 +------------ packages/call/package.json | 5 -- packages/call/src/components/buttons/deaf.tsx | 31 ++------ .../call/src/components/buttons/leave.tsx | 25 ++----- packages/call/src/components/buttons/mute.tsx | 27 ++----- .../src/components/buttons/tooltipButton.tsx | 32 +++++++++ packages/call/src/components/modals/base.tsx | 2 +- packages/call/src/speakingIndicator.ts | 41 +++-------- packages/call/src/speakingState.ts | 55 +++++++++++++++ packages/call/src/store.tsx | 5 -- packages/call/src/values.ts | 11 --- packages/crypto/src/worker.ts | 37 ---------- packages/markdown/package.json | 1 - packages/markdown/src/markdown.tsx | 6 +- packages/notifications/package.json | 2 + packages/shared/package.json | 1 - packages/storage/src/session.tsx | 22 +++--- packages/tauth/package.json | 5 +- packages/ttp/package.json | 3 +- 29 files changed, 168 insertions(+), 338 deletions(-) create mode 100644 apps/web/src/routes/app/useShowMobileNavbar.ts create mode 100644 packages/call/src/components/buttons/tooltipButton.tsx create mode 100644 packages/call/src/speakingState.ts delete mode 100644 packages/call/src/values.ts diff --git a/.fallowrc.json b/.fallowrc.json index 320cbb5..022e0cc 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -1,6 +1,11 @@ { "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", - "entry": ["src/index.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}"], + "entry": [ + "src/index.{ts,tsx,js,jsx}", + "src/main.{ts,tsx,js,jsx}", + "apps/tauri/render-version.ts", + "packages/**/*.test.ts" + ], "workspaces": { "packages": ["packages/*", "apps/*"] }, diff --git a/apps/electron/package.json b/apps/electron/package.json index aa4d5a0..f20bb96 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -26,9 +26,7 @@ "validate:raw": "bun run build && bun run package:linux:raw", "validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run validate:raw; else bun run validate:raw; fi" }, - "dependencies": { - "electron-updater": "^6.6.2" - }, + "dependencies": {}, "devDependencies": { "@types/node": "^25.9.1", "electron": "^39.2.7", diff --git a/apps/electron/src/main/main.ts b/apps/electron/src/main/main.ts index 05f88fc..ebda1e5 100644 --- a/apps/electron/src/main/main.ts +++ b/apps/electron/src/main/main.ts @@ -12,6 +12,7 @@ import { import { checkForUpdates } from "./updates.js"; import { ipcChannels, + type DesktopScreenShareAudioOutput, type DesktopScreenShareCapabilities, } from "../shared/ipc.js"; @@ -88,7 +89,7 @@ function execJson(command: string, args: string[]) { }); } -async function listAudioOutputs() { +async function listAudioOutputs(): Promise { verboseLog("listAudioOutputs", { platform: process.platform }); if (process.platform !== "linux") return []; @@ -106,7 +107,9 @@ async function listAudioOutputs() { if (!id || !name) return null; return { id, name, isDefault: false }; }) - .filter(Boolean); + .filter( + (output): output is DesktopScreenShareAudioOutput => output != null, + ); } async function listScreenShareSources() { diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 91c18bd..fb41de4 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -30,12 +30,10 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-opener": "^2", + "react-dom": "^19.2.0", "@tensamin/ui": "*", "@tensamin/shared": "workspace:*", - "lucide-react": "^1.14.0", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "react": "^19.2.0" }, "devDependencies": { "@tauri-apps/cli": "^2", diff --git a/apps/web/package.json b/apps/web/package.json index 0748a1f..04d731f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "@fontsource-variable/inter": "^5.2.8", - "@noble/curves": "^2.2.0", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", @@ -30,21 +29,13 @@ "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "@tensamin/notifications": "workspace:*", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "comlink": "^4.4.2", - "framer-motion": "^12.38.0", + "tw-animate-css": "^1.4.0", "lucide-react": "^1.14.0", "qrcode": "^1.5.4", "react": "^19.2.0", "react-dom": "^19.2.0", - "shadcn": "^4.6.0", - "sonner": "^2.0.7", - "tailwind-merge": "^3.5.0", - "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.2.4", - "tauri-plugin-app-events-api": "^0.2.0", - "tw-animate-css": "^1.4.0", + "tailwind-scrollbar-hide": "^4.0.0", "zod": "^4.3.6" }, "devDependencies": { diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index 054aff7..c9851a3 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -33,7 +33,7 @@ import { useIsMobile } from "@tensamin/ui"; import { MobileNavbar } from "./navbar"; import SidebarBox from "@tensamin/call/sidebarBox"; -import { useShowMobileNavbar } from "@/routes/app/layout"; +import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar"; import { Ellipsis, Check } from "lucide-react"; import { useState } from "react"; import type { User } from "@tensamin/user/context"; diff --git a/apps/web/src/features/legal/screen.tsx b/apps/web/src/features/legal/screen.tsx index 6d4062c..9859036 100644 --- a/apps/web/src/features/legal/screen.tsx +++ b/apps/web/src/features/legal/screen.tsx @@ -220,7 +220,7 @@ function ContinueButton({ ); } -export function BigCheckbox({ +function BigCheckbox({ id, label, checked, diff --git a/apps/web/src/features/settings/components.tsx b/apps/web/src/features/settings/components.tsx index 7b4157b..b9f2967 100644 --- a/apps/web/src/features/settings/components.tsx +++ b/apps/web/src/features/settings/components.tsx @@ -1,5 +1,5 @@ import { Label } from "@tensamin/ui"; -import { Switch as UISwitch, Input as UIInput } from "@tensamin/ui"; +import { Switch as UISwitch } from "@tensamin/ui"; import { useEffect, useState } from "react"; import type { Storage } from "@tensamin/shared/data"; @@ -10,28 +10,6 @@ type BooleanStorageKey = { [K in keyof Storage]: Storage[K] extends boolean ? K : never; }[keyof Storage]; -type StringStorageKey = { - [K in keyof Storage]: Storage[K] extends string ? K : never; -}[keyof Storage]; - -type NumberStorageKey = { - [K in keyof Storage]: Storage[K] extends number ? K : never; -}[keyof Storage]; - -type InputProps = - | { - label: string; - id: StringStorageKey; - placeholder?: string; - type?: "text"; - } - | { - label: string; - id: NumberStorageKey; - placeholder?: string; - type: "number"; - }; - export function Switch({ label, id, @@ -60,49 +38,3 @@ export function Switch({ ); } - -export function Input({ label, id, placeholder, type }: InputProps) { - const { save, load } = useStorage(); - const [value, setValue] = useState( - type === "number" ? 0 : "", - ); - - useEffect(() => { - load(id).then((loadedValue) => { - if (type === "number") { - if (typeof loadedValue === "number") { - setValue(loadedValue); - } - return; - } - - if (typeof loadedValue === "string") { - setValue(loadedValue); - } - }); - }, [id, load, type]); - - return ( -
- - { - if (type === "number") { - const nextValue = Number(e.target.value); - setValue(nextValue); - save(id, nextValue); - return; - } - - const nextValue = e.target.value; - setValue(nextValue); - save(id, nextValue); - }} - /> -
- ); -} diff --git a/apps/web/src/routes/app/layout.tsx b/apps/web/src/routes/app/layout.tsx index a937d2f..4f5c657 100644 --- a/apps/web/src/routes/app/layout.tsx +++ b/apps/web/src/routes/app/layout.tsx @@ -2,17 +2,12 @@ import { type ReactNode } from "react"; import Sidebar from "@/components/sidebar"; import Navbar, { MobileNavbar } from "@/components/navbar"; +import { useShowMobileNavbar } from "./useShowMobileNavbar"; import { useIsMobile, cn, SidebarProvider } from "@tensamin/ui"; import { isTauri } from "@tauri-apps/api/core"; -import { useLocation, useMatches } from "@tanstack/react-router"; -/** - * Executes Layout. - * @param props Parameter props. - * @returns unknown. - */ export default function Layout({ children }: { children: ReactNode }) { const isMobile = useIsMobile(); const showMobileNavbar = useShowMobileNavbar(); @@ -47,17 +42,3 @@ export default function Layout({ children }: { children: ReactNode }) { ); } - -export function useShowMobileNavbar(): boolean { - const matches = useMatches(); - const location = useLocation(); - - const value = matches.some( - (match) => - match.pathname === location.pathname && - // @ts-expect-error Stuff - match.staticData?.showMobileNavbar === true, - ); - - return value; -} diff --git a/apps/web/src/routes/app/useShowMobileNavbar.ts b/apps/web/src/routes/app/useShowMobileNavbar.ts new file mode 100644 index 0000000..5fb039e --- /dev/null +++ b/apps/web/src/routes/app/useShowMobileNavbar.ts @@ -0,0 +1,14 @@ +import { useLocation, useMatches } from "@tanstack/react-router"; + +export function useShowMobileNavbar(): boolean { + const matches = useMatches(); + const location = useLocation(); + + return matches.some( + (match) => + match.pathname === location.pathname && + // Router staticData is app-defined and not typed by the route matcher here. + // @ts-expect-error App route metadata + match.staticData?.showMobileNavbar === true, + ); +} diff --git a/bun.lock b/bun.lock index 02db475..a1d9171 100644 --- a/bun.lock +++ b/bun.lock @@ -28,9 +28,6 @@ "apps/electron": { "name": "@tensamin/electron", "version": "0.0.3", - "dependencies": { - "electron-updater": "^6.6.2", - }, "devDependencies": { "@types/node": "^25.9.1", "electron": "^39.2.7", @@ -46,10 +43,8 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-opener": "^2", "@tensamin/shared": "workspace:*", "@tensamin/ui": "*", - "lucide-react": "^1.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", }, @@ -63,7 +58,6 @@ "version": "0.0.0", "dependencies": { "@fontsource-variable/inter": "^5.2.8", - "@noble/curves": "^2.2.0", "@tailwindcss/vite": "^4.2.4", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", @@ -80,20 +74,12 @@ "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "comlink": "^4.4.2", - "framer-motion": "^12.38.0", "lucide-react": "^1.14.0", "qrcode": "^1.5.4", "react": "^19.2.0", "react-dom": "^19.2.0", - "shadcn": "^4.6.0", - "sonner": "^2.0.7", - "tailwind-merge": "^3.5.0", "tailwind-scrollbar-hide": "^4.0.0", "tailwindcss": "^4.2.4", - "tauri-plugin-app-events-api": "^0.2.0", "tw-animate-css": "^1.4.0", "zod": "^4.3.6", }, @@ -116,15 +102,12 @@ "version": "0.0.0", "dependencies": { "@livekit/components-react": "^2.9.20", - "@tanstack/react-query": "^5.100.7", "@tanstack/react-router": "^1.169.1", "@tanstack/react-virtual": "^3.13.24", "@tauri-apps/api": "^2", "@tensamin/crypto": "workspace:*", - "@tensamin/markdown": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", - "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", @@ -134,7 +117,6 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": "^3.8.1", - "sonner": "^2.0.7", "zod": "^4.3.6", "zustand": "^5.0.8", }, @@ -177,7 +159,6 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.41.1", - "@tensamin/ui": "*", "react": "^19.2.0", "react-dom": "^19.2.0", }, @@ -193,9 +174,11 @@ "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", "@tensamin/ttp": "workspace:*", + "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "react": "^19.2.0", "react-dom": "^19.2.0", + "sonner": "^2.0.7", "zod": "^4.3.6", }, }, @@ -207,7 +190,6 @@ "lucide-react": "^1.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "sonner": "^2.0.7", "zod": "^4.3.6", }, }, @@ -231,14 +213,12 @@ "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", - "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "lucide-react": "^1.8.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "sonner": "^2.0.7", "zod": "^4.3.6", }, }, @@ -256,7 +236,6 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "tauri-plugin-app-events-api": "^0.2.0", - "zod": "^4.3.6", }, "devDependencies": { "eslint": "^10.0.3", @@ -740,8 +719,6 @@ "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.9", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA=="], - "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="], - "@tensamin/call": ["@tensamin/call@workspace:packages/call"], "@tensamin/chat": ["@tensamin/chat@workspace:packages/chat"], @@ -1122,8 +1099,6 @@ "electron-to-chromium": ["electron-to-chromium@1.5.349", "", {}, "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A=="], - "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], - "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], @@ -1256,8 +1231,6 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="], - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1476,10 +1449,6 @@ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], - "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], - - "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], @@ -1528,10 +1497,6 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="], - - "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.14.2", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-D2bTe0tpuf9nw4DA39wFaqUD/hRPKj0DKpo2lAqu+A47Ifg4+h0hbfn6QxVOsiUY2uhgEN6TTpGSHDsc+ysYNg=="], @@ -1770,7 +1735,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shadcn": ["shadcn@4.6.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-4XeMwFf8ZZxmqQQp+U+Nsq2M+cY4Da8Joo/EaMdHVc4uVuWSTJoeidlZ3gDjyxXCjYB1FLcxYwR4lYQAH8emOg=="], + "shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1850,8 +1815,6 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], - "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], @@ -2054,8 +2017,6 @@ "@tensamin/ui/recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], - "@tensamin/ui/shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], - "@types/cacheable-request/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/fs-extra/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], @@ -2180,10 +2141,6 @@ "@tensamin/tauri/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], - "@tensamin/ui/shadcn/fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], - - "@tensamin/ui/shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@types/cacheable-request/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "@types/fs-extra/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], diff --git a/packages/call/package.json b/packages/call/package.json index c60a47d..080dd15 100644 --- a/packages/call/package.json +++ b/packages/call/package.json @@ -16,15 +16,11 @@ }, "dependencies": { "@livekit/components-react": "^2.9.20", - "@tanstack/react-query": "^5.100.7", "@tanstack/react-router": "^1.169.1", - "@tanstack/react-virtual": "^3.13.24", "@tauri-apps/api": "^2", "@tensamin/crypto": "workspace:*", - "@tensamin/markdown": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", - "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", @@ -34,7 +30,6 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": "^3.8.1", - "sonner": "^2.0.7", "zod": "^4.3.6", "zustand": "^5.0.8" } diff --git a/packages/call/src/components/buttons/deaf.tsx b/packages/call/src/components/buttons/deaf.tsx index 42dd3f4..6d0ede0 100644 --- a/packages/call/src/components/buttons/deaf.tsx +++ b/packages/call/src/components/buttons/deaf.tsx @@ -1,18 +1,14 @@ -import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; +import { Button } from "@tensamin/ui"; import { toggleDeaf, useCall } from "../../store"; import { HeadphoneOff, Headphones } from "lucide-react"; +import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; export default function DeafButton({ className, iconSize, tooltip, portalContainer, -}: { - className?: string; - iconSize?: number; - tooltip?: string; - portalContainer?: HTMLElement; -}) { +}: CallButtonProps) { const deaf = useCall((state) => state.deaf); const button = ( @@ -22,27 +18,12 @@ export default function DeafButton({ className={className} > {deaf ? ( - + ) : ( - + )} ); - if (!tooltip) { - return button; - } - - return ( - - - - {tooltip} - - - ); + return withTooltip(button, tooltip, portalContainer); } diff --git a/packages/call/src/components/buttons/leave.tsx b/packages/call/src/components/buttons/leave.tsx index 364b4be..d4f0371 100644 --- a/packages/call/src/components/buttons/leave.tsx +++ b/packages/call/src/components/buttons/leave.tsx @@ -1,18 +1,14 @@ import { LeaveIcon } from "@livekit/components-react"; -import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; +import { Button } from "@tensamin/ui"; import { disconnect, useCall } from "../../store"; +import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; export default function LeaveButton({ className, iconSize, tooltip, portalContainer, -}: { - className?: string; - iconSize?: number; - tooltip?: string; - portalContainer?: HTMLElement; -}) { +}: CallButtonProps) { const state = useCall((store) => store.state); const button = ( @@ -22,20 +18,9 @@ export default function LeaveButton({ disabled={state === "closing" || state === "closed"} variant="destructive" > - + ); - if (!tooltip) { - return button; - } - - return ( - - - - {tooltip} - - - ); + return withTooltip(button, tooltip, portalContainer); } diff --git a/packages/call/src/components/buttons/mute.tsx b/packages/call/src/components/buttons/mute.tsx index c8f2ba0..3067a2b 100644 --- a/packages/call/src/components/buttons/mute.tsx +++ b/packages/call/src/components/buttons/mute.tsx @@ -1,18 +1,14 @@ -import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; +import { Button } from "@tensamin/ui"; import { toggleMute, useCall } from "../../store"; import { Mic, MicOff } from "lucide-react"; +import { type CallButtonProps, iconScale, withTooltip } from "./tooltipButton"; export default function MuteButton({ className, iconSize, tooltip, portalContainer, -}: { - className?: string; - iconSize?: number; - tooltip?: string; - portalContainer?: HTMLElement; -}) { +}: CallButtonProps) { const micEnabled = useCall((state) => state.micEnabled); const button = ( @@ -22,23 +18,12 @@ export default function MuteButton({ onClick={() => void toggleMute()} > {micEnabled ? ( - + ) : ( - + )} ); - if (!tooltip) { - return button; - } - - return ( - - - - {tooltip} - - - ); + return withTooltip(button, tooltip, portalContainer); } diff --git a/packages/call/src/components/buttons/tooltipButton.tsx b/packages/call/src/components/buttons/tooltipButton.tsx new file mode 100644 index 0000000..72110f8 --- /dev/null +++ b/packages/call/src/components/buttons/tooltipButton.tsx @@ -0,0 +1,32 @@ +import { type ReactElement } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@tensamin/ui"; + +export type CallButtonProps = { + className?: string; + iconSize?: number; + tooltip?: string; + portalContainer?: HTMLElement; +}; + +export function iconScale(iconSize?: number) { + return { scale: (iconSize ? iconSize + 100 : 100) + "%" }; +} + +export function withTooltip( + button: ReactElement, + tooltip?: string, + portalContainer?: HTMLElement, +) { + if (!tooltip) { + return button; + } + + return ( + + + + {tooltip} + + + ); +} diff --git a/packages/call/src/components/modals/base.tsx b/packages/call/src/components/modals/base.tsx index 768f6f5..47e47cf 100644 --- a/packages/call/src/components/modals/base.tsx +++ b/packages/call/src/components/modals/base.tsx @@ -18,7 +18,7 @@ import { import { Track, type Participant } from "livekit-client"; import { useEffect, useRef, useState } from "react"; import { useUser, type User } from "@tensamin/user/context"; -import { useIsSpeaking } from "../../speakingIndicator"; +import { useIsSpeaking } from "../../speakingState"; import VideoViewer from "../videoViewer"; import { HeadphoneOff, MicOff, Monitor, Plus, Shield } from "lucide-react"; diff --git a/packages/call/src/speakingIndicator.ts b/packages/call/src/speakingIndicator.ts index 0c4f5a5..8f05eb8 100644 --- a/packages/call/src/speakingIndicator.ts +++ b/packages/call/src/speakingIndicator.ts @@ -1,5 +1,10 @@ import { log } from "@tensamin/shared/log"; -import { useCall } from "./store"; +import { + clearSpeakingParticipants, + removeSpeakingParticipant, + setMicGated, + updateSpeakingParticipants, +} from "./speakingState"; const SPEAKING_THRESHOLD = 0.01; const SPEAKING_HANGTIME_MS = 500; @@ -85,12 +90,7 @@ class SpeakingDetector { } this.entries.delete(participantId); - useCall.setState((state) => { - if (!state.speakingParticipantIds.has(participantId)) return state; - const next = new Set(state.speakingParticipantIds); - next.delete(participantId); - return { speakingParticipantIds: next }; - }); + removeSpeakingParticipant(participantId); if (participantId === this.localParticipantId && this.localMicGateClosed) { this.muteLocalTrack(false); @@ -104,7 +104,7 @@ class SpeakingDetector { entry.isSpeaking = false; entry.lastSpeakingTime = 0; } - useCall.setState({ speakingParticipantIds: new Set() }); + clearSpeakingParticipants(); } } @@ -131,7 +131,7 @@ class SpeakingDetector { } this.localMicGateClosed = muted; - useCall.setState({ micGated: muted }); + setMicGated(muted); } private applyNoiseGate(rms: number) { @@ -197,24 +197,7 @@ class SpeakingDetector { } if (changed.size > 0) { - useCall.setState((state) => { - let hasDiff = false; - const next = new Set(state.speakingParticipantIds); - for (const [id, speaking] of changed) { - if (speaking) { - if (!next.has(id)) { - next.add(id); - hasDiff = true; - } - } else { - if (next.has(id)) { - next.delete(id); - hasDiff = true; - } - } - } - return hasDiff ? { speakingParticipantIds: next } : state; - }); + updateSpeakingParticipants(changed); } } @@ -246,7 +229,3 @@ export function disposeSpeakingDetector(): void { detectorInstance = null; } } - -export function useIsSpeaking(participantId: number): boolean { - return useCall((state) => state.speakingParticipantIds.has(participantId)); -} diff --git a/packages/call/src/speakingState.ts b/packages/call/src/speakingState.ts new file mode 100644 index 0000000..8f9e815 --- /dev/null +++ b/packages/call/src/speakingState.ts @@ -0,0 +1,55 @@ +import { create } from "zustand"; + +type SpeakingState = { + speakingParticipantIds: Set; + micGated: boolean; +}; + +const useSpeakingState = create(() => ({ + speakingParticipantIds: new Set(), + micGated: false, +})); + +export function setMicGated(micGated: boolean) { + useSpeakingState.setState({ micGated }); +} + +export function clearSpeakingParticipants() { + useSpeakingState.setState({ speakingParticipantIds: new Set() }); +} + +export function removeSpeakingParticipant(participantId: number) { + useSpeakingState.setState((state) => { + if (!state.speakingParticipantIds.has(participantId)) return state; + const next = new Set(state.speakingParticipantIds); + next.delete(participantId); + return { speakingParticipantIds: next }; + }); +} + +export function updateSpeakingParticipants(changed: Map) { + useSpeakingState.setState((state) => { + let hasDiff = false; + const next = new Set(state.speakingParticipantIds); + + for (const [id, speaking] of changed) { + if (speaking) { + if (!next.has(id)) { + next.add(id); + hasDiff = true; + } + } else if (next.has(id)) { + next.delete(id); + hasDiff = true; + } + } + + return hasDiff ? { speakingParticipantIds: next } : state; + }); +} + +export function useIsSpeaking(participantId: number): boolean { + return useSpeakingState((state) => + state.speakingParticipantIds.has(participantId), + ); +} diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 6f3955c..7d3e609 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -109,8 +109,6 @@ type CallStore = { layoutVersion: number; screenRef: React.RefObject | null; runtime: Runtime | null; - speakingParticipantIds: Set; - micGated: boolean; }; let _keyProvider: ExternalE2EEKeyProvider | null = null; @@ -857,7 +855,6 @@ export async function disconnect() { watchedStreamParticipantIds: [], pendingWatchedParticipantIds: [], activeScreenShareParticipantIds: [], - micGated: false, }); getRoom().remoteParticipants.forEach((participant) => { @@ -1079,8 +1076,6 @@ export const useCall = create(() => ({ layoutVersion: 0, screenRef: null, runtime: null, - speakingParticipantIds: new Set(), - micGated: false, })); // Register app-level call listeners and wire React dependencies into the store. diff --git a/packages/call/src/values.ts b/packages/call/src/values.ts deleted file mode 100644 index 0564800..0000000 --- a/packages/call/src/values.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; -import { ttp } from "@tensamin/shared/data"; - -export type RawMessages = z.infer["messages"]; - -export type RawMessage = RawMessages[number]; - -export type LiveMessage = RawMessage & { - failed?: boolean; - localId: string; -}; diff --git a/packages/crypto/src/worker.ts b/packages/crypto/src/worker.ts index da393f6..f22fe42 100644 --- a/packages/crypto/src/worker.ts +++ b/packages/crypto/src/worker.ts @@ -395,49 +395,12 @@ export async function getSharedSecret( return out; }; - /** - * Returns WebCrypto subtle API when available. - * @returns SubtleCrypto instance or undefined. - */ const getSubtle = () => globalThis.crypto?.subtle; - { - /* - const hkdfAesGcmFromShared = async ( - sharedSecret: BufferSource, - infoStr: string - ): Promise => { - const subtle = getSubtle(); - if (!subtle) throw new Error("WebCrypto subtle not available"); - const info = textEncoder.encode(infoStr); - const baseKey = await subtle.importKey( - "raw", - sharedSecret, - "HKDF", - false, - ["deriveKey"] - ); - return await subtle.deriveKey( - { - name: "HKDF", - hash: "SHA-256", - salt: new Uint8Array(0), - info, - }, - baseKey, - { name: "AES-GCM", length: 256 }, - false, - ["encrypt", "decrypt"] - ); - }; - */ - } - const myJwk: JWK = normalizeOkpX448Jwk(ownJwk, "own_jwk"); const peerJwk: JWK = normalizeOkpX448Jwk(otherJwk, "other_jwk"); const subtle = getSubtle(); - //const infoStr = `ECDH-X448-AES-GCM-v1|my=${myJwk.x}|peer=${peerJwk.x}`; if (subtle) { const algorithms = [{ name: "ECDH", namedCurve: "X448" }, { name: "X448" }]; diff --git a/packages/markdown/package.json b/packages/markdown/package.json index 5090799..b82d265 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -17,7 +17,6 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.41.1", - "@tensamin/ui": "*", "react": "^19.2.0", "react-dom": "^19.2.0" } diff --git a/packages/markdown/src/markdown.tsx b/packages/markdown/src/markdown.tsx index 46ebb92..976301a 100644 --- a/packages/markdown/src/markdown.tsx +++ b/packages/markdown/src/markdown.tsx @@ -80,7 +80,7 @@ const INLINE_TOKEN_REGEX = * @param input Parameter input. * @returns InlineNode[]. */ -export function parseInlineNodes(input: string): InlineNode[] { +function parseInlineNodes(input: string): InlineNode[] { const nodes: InlineNode[] = []; let cursor = 0; @@ -369,7 +369,7 @@ export function parseMarkdownBlocks(markdown: string): MarkdownBlock[] { * @param nodes Parameter nodes. * @returns React.ReactNode[]. */ -export function renderInline(nodes: InlineNode[]): React.ReactNode[] { +function renderInline(nodes: InlineNode[]): React.ReactNode[] { return nodes.map((node, index) => { if (node.type === "text") { return node.value; @@ -636,7 +636,7 @@ function readTable( }; } -export const markdownStyles = ` +const markdownStyles = ` .tm-md-root { color: hsl(var(--foreground)); line-height: 1.55; font-size: 0.95rem; } .tm-md-heading { margin: 0.2rem 0 0.35rem; font-weight: 700; line-height: 1.25; } .tm-md-h1 { font-size: 1.65rem; } diff --git a/packages/notifications/package.json b/packages/notifications/package.json index a6b06be..cfd540a 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -19,9 +19,11 @@ "@tensamin/chat": "workspace:*", "@tensamin/storage": "workspace:*", "@tensamin/ttp": "workspace:*", + "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "react": "^19.2.0", "react-dom": "^19.2.0", + "sonner": "^2.0.7", "zod": "^4.3.6" } } diff --git a/packages/shared/package.json b/packages/shared/package.json index c009c34..3d06116 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -21,7 +21,6 @@ "lucide-react": "^1.14.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "sonner": "^2.0.7", "zod": "^4.3.6" } } diff --git a/packages/storage/src/session.tsx b/packages/storage/src/session.tsx index 892f3e6..46f36d6 100644 --- a/packages/storage/src/session.tsx +++ b/packages/storage/src/session.tsx @@ -25,9 +25,15 @@ export default function SessionProvider({ children }: { children: ReactNode }) { const { load, save } = useStorage(); const [contacts, setContacts] = useState([]); const [communities, setCommunities] = useState([]); - const [calls, setCalls] = useState([]); + const [localCalls, setLocalCalls] = useState([]); + const calls = [ + ...freshCalls, + ...localCalls.filter( + (call) => !freshCalls.some((fresh) => fresh.call_id === call.call_id), + ), + ]; - // Get cached data and merge fresh data + // Cached session data fills in items the server did not return freshly. useEffect(() => { load("cached_contacts").then((cachedData) => { setContacts([ @@ -53,16 +59,6 @@ export default function SessionProvider({ children }: { children: ReactNode }) { }); }, [load, freshContacts, freshCommunities]); - useEffect(() => { - setCalls((prevCalls) => [ - ...freshCalls, - ...prevCalls.filter( - (call) => !freshCalls.some((fresh) => fresh.call_id === call.call_id), - ), - ]); - }, [freshCalls]); - - // Save data useEffect(() => { save("cached_contacts", contacts); }, [contacts, save]); @@ -98,7 +94,7 @@ export default function SessionProvider({ children }: { children: ReactNode }) { }; const insertCall = (call: Calls[number]) => { - setCalls((prevCalls) => { + setLocalCalls((prevCalls) => { if (prevCalls.some((prevCall) => prevCall.call_id === call.call_id)) { return prevCalls; } diff --git a/packages/tauth/package.json b/packages/tauth/package.json index 3f5044a..2dd2329 100644 --- a/packages/tauth/package.json +++ b/packages/tauth/package.json @@ -17,14 +17,11 @@ "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", - "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "lucide-react": "^1.8.0", "react": "^19.2.0", - "react-dom": "^19.2.0", - "sonner": "^2.0.7", - "zod": "^4.3.6" + "react-dom": "^19.2.0" } } diff --git a/packages/ttp/package.json b/packages/ttp/package.json index ed09f35..7243f25 100644 --- a/packages/ttp/package.json +++ b/packages/ttp/package.json @@ -21,8 +21,7 @@ "@tauri-apps/api": "^2", "react": "^19.2.0", "react-dom": "^19.2.0", - "tauri-plugin-app-events-api": "^0.2.0", - "zod": "^4.3.6" + "tauri-plugin-app-events-api": "^0.2.0" }, "devDependencies": { "eslint": "^10.0.3" From 1a563f63a2616b90a4c79dc499fef29c03009efb Mon Sep 17 00:00:00 2001 From: Alois Date: Wed, 3 Jun 2026 14:54:34 +0200 Subject: [PATCH 3/9] (feat): add lint script to apps/electron/ --- apps/electron/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/electron/package.json b/apps/electron/package.json index f20bb96..d650270 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -9,6 +9,7 @@ "main": "dist/main/main.js", "scripts": { "clean": "rm -rf dist release", + "lint": "eslint src", "build:web": "cd ../.. && bun run build:web", "build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs", "dev:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron . --verbose", From 7a7ab3ad2e6ab1d1600e54db468d243b25bb0075 Mon Sep 17 00:00:00 2001 From: Alois Date: Wed, 3 Jun 2026 15:25:30 +0200 Subject: [PATCH 4/9] (fix): dependency issue --- bun.lock | 3 +-- packages/tauth/package.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index a1d9171..2e3a763 100644 --- a/bun.lock +++ b/bun.lock @@ -103,7 +103,6 @@ "dependencies": { "@livekit/components-react": "^2.9.20", "@tanstack/react-router": "^1.169.1", - "@tanstack/react-virtual": "^3.13.24", "@tauri-apps/api": "^2", "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", @@ -213,13 +212,13 @@ "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", + "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", "lucide-react": "^1.8.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.3.6", }, }, "packages/ttp": { diff --git a/packages/tauth/package.json b/packages/tauth/package.json index 2dd2329..2e9adf2 100644 --- a/packages/tauth/package.json +++ b/packages/tauth/package.json @@ -17,6 +17,7 @@ "@tensamin/crypto": "workspace:*", "@tensamin/shared": "workspace:*", "@tensamin/storage": "workspace:*", + "@tensamin/tauri": "workspace:*", "@tensamin/ttp": "workspace:*", "@tensamin/ui": "*", "@tensamin/user": "workspace:*", From f4088691c5dc7d407d19f6681801d62b047c1fd9 Mon Sep 17 00:00:00 2001 From: Alois Date: Wed, 3 Jun 2026 16:14:39 +0200 Subject: [PATCH 5/9] (feat): improve profile popup in the navbar, add shared code to that popup --- apps/web/src/components/modals/profile.tsx | 103 ++++++++++++++++++--- apps/web/src/components/navbar.tsx | 4 +- packages/shared/package.json | 1 + packages/shared/src/code.ts | 20 ++++ 4 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 packages/shared/src/code.ts diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx index 6a31e36..c46e4ef 100644 --- a/apps/web/src/components/modals/profile.tsx +++ b/apps/web/src/components/modals/profile.tsx @@ -1,8 +1,62 @@ import type { User } from "@tensamin/user/context"; -import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui"; +import { + Avatar, + AvatarFallback, + AvatarImage, + Button, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@tensamin/ui"; +import { toLossySixDigitCode } from "@tensamin/shared/code"; import Text from "@tensamin/markdown/text"; +import { ChevronDown, ChevronUp, Info } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useCrypto } from "@tensamin/crypto/context"; +import { useStorage } from "@tensamin/storage/context"; +import { useUser } from "@tensamin/user/context"; export default function Profile({ user }: { user: User }) { + const [showAdvancedInformation, setShowAdvancedInformation] = useState(false); + const [sharedSecret, setSharedSecret] = useState(""); + + const { getSharedSecret } = useCrypto(); + const { load } = useStorage(); + const { get } = useUser(); + + useEffect(() => { + let active = true; + + void (async () => { + try { + const ownId = await load("user_id"); + const privateKey = await load("private_key"); + const ownData = await get(ownId); + const secret = await getSharedSecret( + privateKey, + ownData.public_key, + user.public_key, + ); + + if (active) { + setSharedSecret(secret); + } + } catch { + if (active) { + setSharedSecret(""); + } + } + })(); + + return () => { + active = false; + }; + }, [get, getSharedSecret, load, user.public_key]); + + const sharedSecretCode = sharedSecret + ? toLossySixDigitCode(sharedSecret) + : "------"; + return (
@@ -18,17 +72,42 @@ export default function Profile({ user }: { user: User }) {
-
-

- iota: {user.iota_id} -

-

- user: {user.user_id} -

-

- pub key: {user.public_key} -

-
+ + {showAdvancedInformation && ( +
+

+ Shared Code: {sharedSecretCode}{" "} + + } /> + + You can compare this code with the conversation partner to + validate that this chat is E2EE + + +

+

+ Iota ID: {user.iota_id} +

+

+ User ID: {user.user_id} +

+

+ Public Key: {user.public_key} +

+
+ )} ); } diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index 1c48c6c..cb83cb6 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -74,7 +74,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { userId={id} component={(user) => isMobile ? ( -

{user?.display}

+

{user?.display}

) : ( -

{user?.display}

+

{user?.display}

} /> diff --git a/packages/shared/package.json b/packages/shared/package.json index 3d06116..26c0d9f 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "exports": { + "./code": "./src/code.ts", "./data": "./src/data.ts", "./desktopMedia": "./src/desktopMedia.tsx", "./log": "./src/log.tsx", diff --git a/packages/shared/src/code.ts b/packages/shared/src/code.ts new file mode 100644 index 0000000..1f1d10e --- /dev/null +++ b/packages/shared/src/code.ts @@ -0,0 +1,20 @@ +export function toLossySixDigitCode(input: string): string { + let firstHash = 0xdeadbeef; + let secondHash = 0x41c6ce57; + + for (let index = 0; index < input.length; index += 1) { + const character = input.charCodeAt(index); + + firstHash = Math.imul(firstHash ^ character, 2654435761); + secondHash = Math.imul(secondHash ^ character, 1597334677); + } + + firstHash = Math.imul(firstHash ^ (firstHash >>> 16), 2246822507); + firstHash ^= Math.imul(secondHash ^ (secondHash >>> 13), 3266489909); + secondHash = Math.imul(secondHash ^ (secondHash >>> 16), 2246822507); + secondHash ^= Math.imul(firstHash ^ (firstHash >>> 13), 3266489909); + + const hash = 4294967296 * (2097151 & secondHash) + (firstHash >>> 0); + + return String(hash % 1_000_000).padStart(6, "0"); +} From 18e507b43973e8b4b37b88f560fc8bbf5e5fe782 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 4 Jun 2026 22:29:20 +0200 Subject: [PATCH 6/9] (feat): use Inter instead of the system font for ui --- apps/web/src/index.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 8175c9c..925d401 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -7,6 +7,8 @@ @source "../../../packages/**/src/**/*.{ts,tsx}"; @theme { + --font-sans: "Inter Variable", sans-serif; + --animate-wiggle: wiggle 0.5s ease-in-out infinite; @keyframes wiggle { @@ -28,6 +30,10 @@ body, overflow: hidden; } +body { + font-family: "Inter Variable", sans-serif; +} + #root { min-height: 0; } From 4f82b9c85bd1e617cd819f1c727ad88a8cc92481 Mon Sep 17 00:00:00 2001 From: Alois Date: Mon, 8 Jun 2026 11:12:44 +0200 Subject: [PATCH 7/9] (feat): add call popout --- apps/web/src/index.tsx | 2 + bun.lock | 4 +- package.json | 2 +- packages/call/package.json | 3 +- packages/call/src/components/popout.tsx | 485 ++++++++++++++++++++++++ packages/call/src/store.tsx | 5 + 6 files changed, 497 insertions(+), 4 deletions(-) create mode 100644 packages/call/src/components/popout.tsx diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 6487ba1..a26c462 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -21,6 +21,7 @@ import ChatScreen from "@tensamin/chat/screen"; import CallScreen from "@tensamin/call/screen"; import Login from "@/routes/screens/login"; +import CallPopout from "@tensamin/call/popout"; import ChatContext from "@tensamin/chat/context"; import { useInitializeCall } from "@tensamin/call/store"; import { Provider as TTPProvider } from "@tensamin/ttp"; @@ -147,6 +148,7 @@ function AppShell() { + diff --git a/bun.lock b/bun.lock index 2e3a763..92a897c 100644 --- a/bun.lock +++ b/bun.lock @@ -254,7 +254,7 @@ }, }, "overrides": { - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz", + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", }, "packages": { @@ -740,7 +740,7 @@ "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], - "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-La9VqXqJFtzzsRQotXVp+3Vr6u8kj4mQ4wTlSIMRDxKFBnbCvtZyB3V/f8HiIlf7FlZ0Xg0suUUpnamvtcvs9w=="], + "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-08pYuWTivuDWNiBoSSY2P8rIZz4FvuvLQZMjaQnf7KE/Pz9bGO69XHFcCHQXIz1SWsT2TyjochT0sPEGAta15g=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "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-5zql8LtBKVn7WuQDCIdLbthKvLEyxjWShAceXCYbk+kwO41VVILhS1EWuDvjgvv2BBdyuxoVWvdblPIW4BcdSQ=="], diff --git a/package.json b/package.json index 1c2b52d..b89f348 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "overrides": { "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", - "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz" + "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.20.tar.gz" }, "dependencies": { "@tensamin/ttp-core": "*", diff --git a/packages/call/package.json b/packages/call/package.json index 080dd15..d3b5827 100644 --- a/packages/call/package.json +++ b/packages/call/package.json @@ -7,7 +7,8 @@ "./store": "./src/store.tsx", "./screen": "./src/screen.tsx", "./utils": "./src/utils.ts", - "./sidebarBox": "./src/components/sidebarBox.tsx" + "./sidebarBox": "./src/components/sidebarBox.tsx", + "./popout": "./src/components/popout.tsx" }, "scripts": { "format": "bunx prettier --write .", diff --git a/packages/call/src/components/popout.tsx b/packages/call/src/components/popout.tsx new file mode 100644 index 0000000..1327c9c --- /dev/null +++ b/packages/call/src/components/popout.tsx @@ -0,0 +1,485 @@ +import { Participant, Track } from "livekit-client"; +import VideoViewer from "./videoViewer"; +import { useLocation } from "@tanstack/react-router"; +import { getRoom, stopWatchingStream, useCall } from "../store"; +import { useState, useRef, useEffect, useCallback } from "react"; +import { Button, cn } from "@tensamin/ui"; +import { ScreenShareOff } from "lucide-react"; + +function getTrackPublicationBySource( + participant: Participant | undefined, + source: Track.Source, +) { + if (!participant) { + return undefined; + } + + return [...participant.trackPublications.values()].find( + (publication) => publication.source === source, + ); +} + +type Positions = "top-left" | "top-right" | "bottom-left" | "bottom-right"; +type ResizeEdge = + | "top" + | "right" + | "bottom" + | "left" + | "top-left" + | "top-right" + | "bottom-right" + | "bottom-left"; +type Point = { + x: number; + y: number; +}; + +const MARGIN = 40; +const MIN_SIZE = 240; +const MAX_SIZE = 1000; +const ASPECT_RATIO = 9 / 16; + +export function Popout({ participant }: { participant: Participant }) { + const screenSharePublication = getTrackPublicationBySource( + participant, + Track.Source.ScreenShare, + ); + + const popoutRef = useRef(null); + const coordsRef = useRef({ x: MARGIN, y: MARGIN }); + const dragOffsetRef = useRef({ x: 0, y: 0 }); + const sizeRef = useRef(400); + const hideOverlayTimeoutRef = useRef | null>( + null, + ); + const resizeRef = useRef<{ + size: number; + clientX: number; + clientY: number; + edge: ResizeEdge; + }>({ size: 400, clientX: 0, clientY: 0, edge: "right" }); + + const [size, setSize] = useState(400); + const [isDragging, setIsDragging] = useState(false); + const [isResizing, setIsResizing] = useState(false); + const [isOverlayVisible, setIsOverlayVisible] = useState(false); + const [position, setPosition] = useState("top-left"); + const [coords, setCoords] = useState({ x: MARGIN, y: MARGIN }); + + const setCoordsSafe = (next: Point) => { + coordsRef.current = next; + setCoords(next); + }; + + const setSizeSafe = (next: number) => { + sizeRef.current = next; + setSize(next); + }; + + const getMaxSize = useCallback(() => { + const maxWidth = window.innerWidth - MARGIN * 2; + const maxHeightWidth = (window.innerHeight - MARGIN * 2) / ASPECT_RATIO; + + return Math.max(MIN_SIZE, Math.min(MAX_SIZE, maxWidth, maxHeightWidth)); + }, []); + + const clampSize = useCallback( + (nextSize: number) => { + return Math.min(getMaxSize(), Math.max(MIN_SIZE, nextSize)); + }, + [getMaxSize], + ); + + const getPositionFromPoint = ( + clientX: number, + clientY: number, + ): Positions => { + const height = window.innerHeight / 2 > clientY ? "top" : "bottom"; + const width = window.innerWidth / 2 > clientX ? "left" : "right"; + + return `${height}-${width}` as Positions; + }; + + const getCoordsForPosition = useCallback( + (nextPosition: Positions, nextSize = size): Point => { + const width = nextSize; + const height = nextSize * ASPECT_RATIO; + + return { + x: nextPosition.endsWith("right") + ? window.innerWidth - width - MARGIN + : MARGIN, + y: nextPosition.startsWith("bottom") + ? window.innerHeight - height - MARGIN + : MARGIN, + }; + }, + [size], + ); + + useEffect(() => { + if (isDragging) return; + + // eslint-disable-next-line + setCoordsSafe(getCoordsForPosition(position)); + }, [position, size, isDragging, getCoordsForPosition]); + + useEffect(() => { + const handleResize = () => { + if (isDragging) return; + + const nextSize = clampSize(size); + + setSizeSafe(nextSize); + setCoordsSafe(getCoordsForPosition(position, nextSize)); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [position, size, isDragging, getCoordsForPosition, clampSize]); + + useEffect(() => { + return () => { + if (hideOverlayTimeoutRef.current) { + clearTimeout(hideOverlayTimeoutRef.current); + } + }; + }, []); + + const scheduleOverlayHide = () => { + if (hideOverlayTimeoutRef.current) { + clearTimeout(hideOverlayTimeoutRef.current); + } + + hideOverlayTimeoutRef.current = setTimeout(() => { + setIsOverlayVisible(false); + hideOverlayTimeoutRef.current = null; + }, 3000); + }; + + const showOverlay = () => { + setIsOverlayVisible(true); + scheduleOverlayHide(); + }; + + const hideOverlay = () => { + if (hideOverlayTimeoutRef.current) { + clearTimeout(hideOverlayTimeoutRef.current); + hideOverlayTimeoutRef.current = null; + } + + setIsOverlayVisible(false); + }; + + const getCornerResizeDelta = ( + e: React.PointerEvent, + horizontalDelta: number, + verticalDelta: number, + ) => { + return Math.abs(horizontalDelta) > Math.abs(verticalDelta) + ? horizontalDelta + : verticalDelta; + }; + + const getResizeDelta = (e: React.PointerEvent, edge: ResizeEdge) => { + switch (edge) { + case "left": + return resizeRef.current.clientX - e.clientX; + case "right": + return e.clientX - resizeRef.current.clientX; + case "top": + return (resizeRef.current.clientY - e.clientY) / ASPECT_RATIO; + case "bottom": + return (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO; + case "top-left": + return getCornerResizeDelta( + e, + resizeRef.current.clientX - e.clientX, + (resizeRef.current.clientY - e.clientY) / ASPECT_RATIO, + ); + case "top-right": + return getCornerResizeDelta( + e, + e.clientX - resizeRef.current.clientX, + (resizeRef.current.clientY - e.clientY) / ASPECT_RATIO, + ); + case "bottom-right": + return getCornerResizeDelta( + e, + e.clientX - resizeRef.current.clientX, + (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO, + ); + case "bottom-left": + return getCornerResizeDelta( + e, + resizeRef.current.clientX - e.clientX, + (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO, + ); + } + }; + + const startResize = ( + e: React.PointerEvent, + edge: ResizeEdge, + ) => { + if (e.button !== 0) return; + + e.stopPropagation(); + e.currentTarget.setPointerCapture(e.pointerId); + + resizeRef.current = { + size, + clientX: e.clientX, + clientY: e.clientY, + edge, + }; + + setIsResizing(true); + }; + + const resizePopout = (e: React.PointerEvent) => { + if (!isResizing) return; + + const nextSize = clampSize( + resizeRef.current.size + getResizeDelta(e, resizeRef.current.edge), + ); + + setSizeSafe(nextSize); + setCoordsSafe(getCoordsForPosition(position, nextSize)); + }; + + const stopResize = (e: React.PointerEvent) => { + if (!isResizing) return; + + e.stopPropagation(); + setIsResizing(false); + setCoordsSafe(getCoordsForPosition(position, sizeRef.current)); + }; + + const resizeEdgeClassName = "absolute z-10 bg-transparent"; + const resizeCornerClassName = "absolute z-20 h-4 w-4 bg-transparent"; + + if (!screenSharePublication) { + return null; + } + + return ( +
{ + if (e.button !== 0) return; + if (isResizing) return; + + e.currentTarget.setPointerCapture(e.pointerId); + + const current = coordsRef.current; + + dragOffsetRef.current = { + x: e.clientX - current.x, + y: e.clientY - current.y, + }; + + setIsDragging(true); + }} + onPointerMove={(e) => { + if (!isDragging) return; + + setCoordsSafe({ + x: e.clientX - dragOffsetRef.current.x, + y: e.clientY - dragOffsetRef.current.y, + }); + }} + onPointerUp={(e) => { + if (!isDragging) return; + + const nextPosition = getPositionFromPoint(e.clientX, e.clientY); + + setPosition(nextPosition); + setIsDragging(false); + + requestAnimationFrame(() => { + setCoordsSafe(getCoordsForPosition(nextPosition)); + }); + }} + onPointerCancel={() => { + setIsDragging(false); + + requestAnimationFrame(() => { + setCoordsSafe(getCoordsForPosition(position)); + }); + }} + onMouseEnter={showOverlay} + onMouseMove={showOverlay} + onMouseLeave={hideOverlay} + className={cn( + "fixed left-0 top-0 aspect-video z-200 rounded-lg border-2 border-muted-foreground bg-black", + "select-none touch-none", + isDragging ? "cursor-grabbing" : "cursor-grab", + )} + style={{ + width: size, + transform: `translate3d(${coords.x}px, ${coords.y}px, 0)`, + transition: + isDragging || isResizing + ? "none" + : "transform 420ms cubic-bezier(0.34, 1.56, 0.64, 1)", + willChange: "transform", + }} + > + +
+
+
+
+
+
+ +
+
+
startResize(e, "top")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "right")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "bottom")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "left")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "top-left")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "top-right")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "bottom-right")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
startResize(e, "bottom-left")} + onPointerMove={resizePopout} + onPointerUp={stopResize} + onPointerCancel={stopResize} + /> +
+ ); +} + +export default function Wrapper() { + const room = getRoom(); + const { pathname } = useLocation(); + const state = useCall((state) => state.state); + const watchedStreamParticipantIds = useCall( + (state) => state.watchedStreamParticipantIds, + ); + const lastFocusedParticipantId = useCall( + (state) => state.lastFocusedParticipantId, + ); + const participant = room.getParticipantByIdentity( + String(lastFocusedParticipantId), + ); + + if (!participant || !lastFocusedParticipantId) { + return null; + } + + const active = + !pathname.startsWith("/call") && + state === "open" && + watchedStreamParticipantIds.includes(Number(participant.identity ?? 0)); + + return active && ; +} diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 7d3e609..efa3802 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -109,6 +109,7 @@ type CallStore = { layoutVersion: number; screenRef: React.RefObject | null; runtime: Runtime | null; + lastFocusedParticipantId: number | null; }; let _keyProvider: ExternalE2EEKeyProvider | null = null; @@ -695,6 +696,7 @@ export function focusParticipant( ) { useCall.setState({ focusedParticipantId: participantId, + lastFocusedParticipantId: participantId, focusedParticipantType: type, view: "focused", }); @@ -855,6 +857,7 @@ export async function disconnect() { watchedStreamParticipantIds: [], pendingWatchedParticipantIds: [], activeScreenShareParticipantIds: [], + lastFocusedParticipantId: null, }); getRoom().remoteParticipants.forEach((participant) => { @@ -1016,6 +1019,7 @@ export function resetCallState() { watchedStreamParticipantIds: [], pendingWatchedParticipantIds: [], activeScreenShareParticipantIds: [], + lastFocusedParticipantId: null, }); syncParticipantState(); @@ -1076,6 +1080,7 @@ export const useCall = create(() => ({ layoutVersion: 0, screenRef: null, runtime: null, + lastFocusedParticipantId: null, })); // Register app-level call listeners and wire React dependencies into the store. From 28a3d72fad91de3c4f29ae12e853e8905f244c3b Mon Sep 17 00:00:00 2001 From: Alois Date: Mon, 8 Jun 2026 13:41:36 +0200 Subject: [PATCH 8/9] (feat): remove homepage dev buttons (fix): some call related bugs (qol): update todo --- apps/web/src/components/navbar.tsx | 29 +++--- apps/web/src/routes/app/home.tsx | 11 -- packages/call/src/components/actions.tsx | 7 +- packages/call/src/components/top.tsx | 2 +- packages/call/src/screen.tsx | 122 ++++++++++++++++++++++- packages/call/src/store.tsx | 1 + packages/call/src/views/main/layout.tsx | 31 +++++- packages/call/todo.md | 1 + 8 files changed, 175 insertions(+), 29 deletions(-) diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index cb83cb6..7550b28 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -33,8 +33,6 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { const isMobile = useIsMobile(); - const [selectOpen, setSelectOpen] = useState(false); - const [userInfoOpen, setUserInfoOpen] = useState(false); return ( @@ -86,7 +84,9 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { textDecorationLine: "none", }} > -

{user?.display}

+

+ {user?.display} +

} /> @@ -131,15 +131,20 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) { ) : ( <> - - + ( + + )} + /> + {currentCalls.map((call) => ( - -
); } diff --git a/packages/call/src/components/actions.tsx b/packages/call/src/components/actions.tsx index 0aa7220..ad990c3 100644 --- a/packages/call/src/components/actions.tsx +++ b/packages/call/src/components/actions.tsx @@ -53,10 +53,13 @@ export default function Actions() { }, [screenRef]); const toggleFullscreen = async () => { + const screen = screenRef?.current; + const fullscreenDocument = screen?.ownerDocument ?? document; + if (callIsFullscreen) { - await document.exitFullscreen().catch(() => undefined); + await fullscreenDocument.exitFullscreen().catch(() => undefined); } else { - await screenRef?.current?.requestFullscreen().catch(() => undefined); + await screen?.requestFullscreen().catch(() => undefined); } triggerCallLayoutCalculation(); diff --git a/packages/call/src/components/top.tsx b/packages/call/src/components/top.tsx index 610493a..4da11db 100644 --- a/packages/call/src/components/top.tsx +++ b/packages/call/src/components/top.tsx @@ -75,7 +75,7 @@ export default function TopBar() { - + {user.display.slice(0, 2).toUpperCase()} diff --git a/packages/call/src/screen.tsx b/packages/call/src/screen.tsx index 49c6a11..433f5ec 100644 --- a/packages/call/src/screen.tsx +++ b/packages/call/src/screen.tsx @@ -1,11 +1,18 @@ -import { useCall } from "./store"; +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { + setCallIsFullscreen, + setCallIsPopout, + triggerCallLayoutCalculation, + useCall, +} from "./store"; import MainLayout from "./views/main/layout"; import MainGrid from "./views/main/grid"; import MainFocused from "./views/main/focused"; import Preview from "./views/preview"; -export default function Screen() { +function ScreenContent() { const view = useCall((state) => state.view); return view === "preview" ? ( @@ -14,3 +21,114 @@ export default function Screen() { {view === "grid" ? : } ); } + +// Popout Window +function copyDocumentStyles(targetDocument: Document) { + for (const node of document.querySelectorAll( + 'link[rel="stylesheet"], style', + )) { + targetDocument.head.appendChild(node.cloneNode(true)); + } +} + +function syncDocumentClasses(targetDocument: Document) { + targetDocument.documentElement.className = document.documentElement.className; + targetDocument.body.className = document.body.className; +} + +function PopoutScreen() { + const closeCheckRef = useRef | null>( + null, + ); + const [container, setContainer] = useState(null); + + useEffect(() => { + const popoutWindow = window.open( + "", + "tensamin-call-popout", + "popup,width=1280,height=720", + ); + + if (!popoutWindow) { + setCallIsFullscreen(false); + setCallIsPopout(false); + return; + } + + let isMounted = true; + + popoutWindow.document.title = document.title; + popoutWindow.document.body.innerHTML = ""; + popoutWindow.document.body.style.margin = "0"; + popoutWindow.document.documentElement.style.height = "100%"; + popoutWindow.document.body.style.height = "100%"; + + copyDocumentStyles(popoutWindow.document); + syncDocumentClasses(popoutWindow.document); + + const containerElement = popoutWindow.document.createElement("div"); + containerElement.style.width = "100%"; + containerElement.style.height = "100%"; + popoutWindow.document.body.appendChild(containerElement); + queueMicrotask(() => { + if (isMounted) { + setContainer(containerElement); + } + }); + + const closePopout = () => { + setCallIsFullscreen(false); + setCallIsPopout(false); + }; + const syncLayout = () => triggerCallLayoutCalculation(); + + popoutWindow.addEventListener("beforeunload", closePopout); + popoutWindow.addEventListener("resize", syncLayout); + popoutWindow.focus(); + + closeCheckRef.current = window.setInterval(() => { + if (popoutWindow.closed) { + closePopout(); + } + }, 500); + + triggerCallLayoutCalculation(); + + return () => { + isMounted = false; + popoutWindow.removeEventListener("beforeunload", closePopout); + popoutWindow.removeEventListener("resize", syncLayout); + + if (closeCheckRef.current) { + window.clearInterval(closeCheckRef.current); + closeCheckRef.current = null; + } + + if (!popoutWindow.closed) { + popoutWindow.close(); + } + + setCallIsFullscreen(false); + triggerCallLayoutCalculation(); + }; + }, []); + + if (!container) { + return null; + } + + return createPortal(, container); +} + +export default function Screen() { + const callIsPopout = useCall((state) => state.callIsPopout); + + return callIsPopout ? ( +
+

The window is popped out.

+ +
+ ) : ( + + ); +} diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index efa3802..a5b4b83 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -857,6 +857,7 @@ export async function disconnect() { watchedStreamParticipantIds: [], pendingWatchedParticipantIds: [], activeScreenShareParticipantIds: [], + callIsFullscreen: false, lastFocusedParticipantId: null, }); diff --git a/packages/call/src/views/main/layout.tsx b/packages/call/src/views/main/layout.tsx index b3710da..f20e233 100644 --- a/packages/call/src/views/main/layout.tsx +++ b/packages/call/src/views/main/layout.tsx @@ -1,7 +1,12 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import Actions from "../../components/actions"; import TopBar from "../../components/top"; -import { setScreenRef, useCall } from "../../store"; +import { + setCallIsFullscreen, + setScreenRef, + triggerCallLayoutCalculation, + useCall, +} from "../../store"; export default function Layout({ children }: { children: React.ReactNode }) { const screenRef = useRef(null); @@ -16,6 +21,30 @@ export default function Layout({ children }: { children: React.ReactNode }) { useEffect(() => { setScreenRef(screenRef); + + const screen = screenRef.current; + const fullscreenDocument = screen?.ownerDocument; + + if (!screen || !fullscreenDocument) { + return; + } + + const handleFullscreenChange = () => { + setCallIsFullscreen(fullscreenDocument.fullscreenElement === screen); + triggerCallLayoutCalculation(); + }; + + fullscreenDocument.addEventListener( + "fullscreenchange", + handleFullscreenChange, + ); + + return () => { + fullscreenDocument.removeEventListener( + "fullscreenchange", + handleFullscreenChange, + ); + }; }, []); const usersInFocusedViewHidden = useCall( diff --git a/packages/call/todo.md b/packages/call/todo.md index 2d8db1b..f44c6fd 100644 --- a/packages/call/todo.md +++ b/packages/call/todo.md @@ -11,3 +11,4 @@ - Context menus - Popout Window - Add quality selection +- Good preview page From 9edd5484666a96b318f4f19c02f7beb12018fee0 Mon Sep 17 00:00:00 2001 From: Alois Date: Mon, 8 Jun 2026 13:43:28 +0200 Subject: [PATCH 9/9] (fix): two small errors --- packages/call/src/components/popout.tsx | 5 ----- packages/call/src/screen.tsx | 4 +--- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/call/src/components/popout.tsx b/packages/call/src/components/popout.tsx index 1327c9c..20611ca 100644 --- a/packages/call/src/components/popout.tsx +++ b/packages/call/src/components/popout.tsx @@ -172,7 +172,6 @@ export function Popout({ participant }: { participant: Participant }) { }; const getCornerResizeDelta = ( - e: React.PointerEvent, horizontalDelta: number, verticalDelta: number, ) => { @@ -193,25 +192,21 @@ export function Popout({ participant }: { participant: Participant }) { return (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO; case "top-left": return getCornerResizeDelta( - e, resizeRef.current.clientX - e.clientX, (resizeRef.current.clientY - e.clientY) / ASPECT_RATIO, ); case "top-right": return getCornerResizeDelta( - e, e.clientX - resizeRef.current.clientX, (resizeRef.current.clientY - e.clientY) / ASPECT_RATIO, ); case "bottom-right": return getCornerResizeDelta( - e, e.clientX - resizeRef.current.clientX, (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO, ); case "bottom-left": return getCornerResizeDelta( - e, resizeRef.current.clientX - e.clientX, (e.clientY - resizeRef.current.clientY) / ASPECT_RATIO, ); diff --git a/packages/call/src/screen.tsx b/packages/call/src/screen.tsx index 433f5ec..975360e 100644 --- a/packages/call/src/screen.tsx +++ b/packages/call/src/screen.tsx @@ -37,9 +37,7 @@ function syncDocumentClasses(targetDocument: Document) { } function PopoutScreen() { - const closeCheckRef = useRef | null>( - null, - ); + const closeCheckRef = useRef(null); const [container, setContainer] = useState(null); useEffect(() => {