diff --git a/apps/pwa/src/vite.ts b/apps/pwa/src/vite.ts index 962dce1..107e08f 100644 --- a/apps/pwa/src/vite.ts +++ b/apps/pwa/src/vite.ts @@ -92,6 +92,11 @@ function emitIcons(): Plugin { attrs: { name: "apple-mobile-web-app-capable", content: "yes" }, injectTo: "head", }, + { + tag: "meta", + attrs: { name: "mobile-web-app-capable", content: "yes" }, + injectTo: "head", + }, { tag: "meta", attrs: { diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index d404d61..cdc1332 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -5139,7 +5139,7 @@ dependencies = [ name = "tensamin" version = "0.0.0" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "jni 0.22.4", "mtp", "reqwest", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index 2a68e95..69fbe80 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -base64 = "0.22" +base64 = "0.23" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] } diff --git a/apps/web/package.json b/apps/web/package.json index 9165eb8..e6b015e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,6 +24,7 @@ "@tensamin/chat": "workspace:*", "@tensamin/crypto": "workspace:*", "@tensamin/hotkeys": "workspace:*", + "@tensamin/markdown": "workspace:*", "@tensamin/mtp": "workspace:*", "@tensamin/notifications": "workspace:*", "@tensamin/onboarding": "workspace:*", diff --git a/apps/web/src/components/callPopout.tsx b/apps/web/src/components/callPopout.tsx new file mode 100644 index 0000000..79d5fb7 --- /dev/null +++ b/apps/web/src/components/callPopout.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense } from "react"; + +import { useCall } from "@tensamin/call/state"; + +const Popout = lazy(() => import("@tensamin/call/popout")); + +export default function CallPopout() { + const active = useCall((state) => state.state !== "closed"); + + return active ? ( + + + + ) : null; +} diff --git a/apps/web/src/components/callRuntimeInit.tsx b/apps/web/src/components/callRuntimeInit.tsx new file mode 100644 index 0000000..296057d --- /dev/null +++ b/apps/web/src/components/callRuntimeInit.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState } from "react"; + +import { useTheme } from "@methanium/ui"; +import { useIsSpeaking } from "@tensamin/call/speakingState"; +import { useCall, useInitializeCall } from "@tensamin/call/store"; +import { useStorage } from "@tensamin/storage/context"; + +function createCallTrayIcon(color: string, speaking: boolean) { + const canvas = document.createElement("canvas"); + canvas.width = 32; + canvas.height = 32; + + const context = canvas.getContext("2d"); + if (!context) return undefined; + + context.globalAlpha = speaking ? 1 : 0.55; + context.fillStyle = color; + context.beginPath(); + context.arc(16, 16, 13, 0, Math.PI * 2); + context.fill(); + + if (speaking) { + context.globalAlpha = 0.3; + context.fillStyle = "#ffffff"; + context.fill(); + } + + return canvas.toDataURL("image/png"); +} + +export default function CallRuntimeInit() { + const callInvitePopup = useInitializeCall(); + const { load } = useStorage(); + const { + themeColor, + themePalette, + themePrimaryColor, + themePolarity, + themeTint, + themeCustomCss, + } = useTheme(); + const [localUserId, setLocalUserId] = useState(-1); + const [primaryColor, setPrimaryColor] = useState(""); + const inCall = useCall((state) => state.state === "open"); + const speaking = useIsSpeaking(localUserId); + + useEffect(() => { + let active = true; + + load("user_id").then((userId) => { + if (active) setLocalUserId(userId); + }); + + return () => { + active = false; + }; + }, [load]); + + useEffect(() => { + const frame = requestAnimationFrame(() => { + setPrimaryColor( + getComputedStyle(document.documentElement) + .getPropertyValue("--primary") + .trim(), + ); + }); + + return () => cancelAnimationFrame(frame); + }, [ + themeColor, + themeCustomCss, + themePalette, + themePolarity, + themePrimaryColor, + themeTint, + ]); + + useEffect(() => { + const iconDataUrl = primaryColor + ? createCallTrayIcon(primaryColor, speaking) + : undefined; + + void window.tensaminDesktop?.call + ?.setStatus?.({ + inCall, + speaking: inCall && speaking, + iconDataUrl: inCall ? iconDataUrl : undefined, + }) + .catch((error: unknown) => { + console.error("Failed to update desktop call status", error); + }); + }, [inCall, primaryColor, speaking]); + + return callInvitePopup; +} diff --git a/apps/web/src/components/callSidebarBox.tsx b/apps/web/src/components/callSidebarBox.tsx new file mode 100644 index 0000000..5480af2 --- /dev/null +++ b/apps/web/src/components/callSidebarBox.tsx @@ -0,0 +1,15 @@ +import { lazy, Suspense } from "react"; + +import { useCall } from "@tensamin/call/state"; + +const SidebarBox = lazy(() => import("@tensamin/call/sidebarBox")); + +export default function CallSidebarBox() { + const active = useCall((state) => state.state !== "closed"); + + return active ? ( + + + + ) : null; +} diff --git a/apps/web/src/components/modals/profile.tsx b/apps/web/src/components/modals/profile.tsx index 1346b19..f077783 100644 --- a/apps/web/src/components/modals/profile.tsx +++ b/apps/web/src/components/modals/profile.tsx @@ -1,6 +1,6 @@ import type { User } from "@tensamin/identity/context"; import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui"; -import { Text } from "@methanium/ui/markdown"; +import Text from "@tensamin/markdown/text"; import { ChevronDown, ChevronUp } from "lucide-react"; import { useState } from "react"; diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index b92d30e..19c367f 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -14,7 +14,7 @@ import { User, } from "lucide-react"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; -import { joinCall, useCall } from "@tensamin/call/store"; +import { useCall } from "@tensamin/call/state"; import Wrapper from "@tensamin/identity/wrapper"; import { Skeleton } from "@methanium/ui"; import { @@ -27,7 +27,7 @@ import { displayCallId } from "@tensamin/call/utils"; import { useState } from "react"; import { SidebarTrigger, useSidebar } from "@methanium/ui"; import { WindowControls as Controls } from "@methanium/ui"; -import { useSession } from "@tensamin/storage/session"; +import { useSession } from "@tensamin/identity/session"; import Profile from "./modals/profile"; export default function Navbar({ forMobile }: { forMobile: boolean }) { @@ -140,7 +140,9 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {