diff --git a/apps/web/src/routes/settings/profile.tsx b/apps/web/src/routes/settings/profile.tsx
index 0b98f2d..e46cc62 100644
--- a/apps/web/src/routes/settings/profile.tsx
+++ b/apps/web/src/routes/settings/profile.tsx
@@ -1,3 +1,211 @@
+import { useStorage } from "@tensamin/storage/context";
+import {
+ Avatar,
+ AvatarFallback,
+ AvatarImage,
+ Button,
+ Input,
+} from "@tensamin/ui";
+import { useUser, type User } from "@tensamin/user/context";
+import { useEffect, useRef, useState } from "react";
+import MDInput from "@tensamin/markdown/input";
+import { ttp } from "@tensamin/shared/data";
+import { useTTP } from "@tensamin/ttp";
+import { Check } from "lucide-react";
+
+async function prepImage(
+ file: File,
+ size = 300,
+ quality = 0.8,
+): Promise
{
+ const bitmap = await createImageBitmap(file);
+
+ const canvas = document.createElement("canvas");
+ canvas.width = size;
+ canvas.height = size;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) throw new Error("Could not get canvas context");
+
+ const scale = Math.max(size / bitmap.width, size / bitmap.height);
+ const width = bitmap.width * scale;
+ const height = bitmap.height * scale;
+ const x = (size - width) / 2;
+ const y = (size - height) / 2;
+
+ ctx.drawImage(bitmap, x, y, width, height);
+
+ return canvas.toDataURL("image/webp", quality);
+}
+
export default function Page() {
- return ;
+ const { get } = useUser();
+ const { load } = useStorage();
+ const { send } = useTTP();
+ const [currentUser, setCurrentUser] = useState(null);
+ const [draftUser, setDraftUser] = useState>({});
+ const [errorMessage, setErrorMessage] = useState("");
+ const [saveSucceeded, setSaveSucceeded] = useState(false);
+ const avatarUploadRef = useRef(null);
+ const draftInitializedRef = useRef(false);
+ const effectiveAvatar =
+ draftUser.avatar === "none" ? undefined : draftUser.avatar;
+
+ const updateDraftUser = (
+ updater: (previous: Partial) => Partial,
+ ) => {
+ setSaveSucceeded(false);
+ setErrorMessage("");
+ setDraftUser(updater);
+ };
+
+ useEffect(() => {
+ const fetchUser = async () => {
+ const user = await get(await load("user_id"));
+ setCurrentUser(user);
+ };
+
+ fetchUser();
+ }, [load, get]);
+
+ useEffect(() => {
+ if (!currentUser || draftInitializedRef.current) return;
+
+ setDraftUser(currentUser);
+ draftInitializedRef.current = true;
+ }, [currentUser]);
+
+ const handleAvatarUpload = async (file: File) => {
+ const final = await prepImage(file);
+ updateDraftUser((prev) => ({ ...prev, avatar: final }));
+ if (avatarUploadRef.current) {
+ avatarUploadRef.current.value = "";
+ }
+ };
+
+ return currentUser ? (
+ <>
+
+ e.target.files?.[0] && handleAvatarUpload(e.target.files[0])
+ }
+ type="file"
+ />
+
+
+
+
+
+ {draftUser.display?.slice(0, 2).toUpperCase() ||
+ currentUser.display.slice(0, 2).toUpperCase()}
+
+
+
+
Avatar
+
+
+
+
+
+ GIFs are supported in decentralised mode or with Tensamin Premium
+
+ Maximum file size is 16mb.
+
+
+
+
+ updateDraftUser((prev) => ({
+ ...prev,
+ display: event.target.value,
+ }))
+ }
+ placeholder="Display Name"
+ value={draftUser.display || ""}
+ />
+
+ updateDraftUser((prev) => ({
+ ...prev,
+ username: event.target.value,
+ }))
+ }
+ placeholder="Username"
+ value={draftUser.username || ""}
+ />
+
+ updateDraftUser((prev) => ({ ...prev, about: value }))
+ }
+ value={draftUser.about || ""}
+ />
+
+ {errorMessage && (
+ {errorMessage}
+ )}
+
+ >
+ ) : (
+ Loading...
+ );
}
diff --git a/bun.lock b/bun.lock
index 325be20..ff51499 100644
--- a/bun.lock
+++ b/bun.lock
@@ -55,6 +55,7 @@
"@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*",
+ "@tensamin/markdown": "workspace:*",
"@tensamin/notifications": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*",
@@ -258,7 +259,7 @@
},
},
"overrides": {
- "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz",
+ "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz",
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz",
},
"packages": {
@@ -698,7 +699,7 @@
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"],
- "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.17.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-smyx+04hSnWM1oyWJfJrRWmxu7OInwyHeIlaD1h3tUrkrSxiKWHl1qCmxVO1bC+cDwyUXhJ5HUnNCbx1aWx7Dg=="],
+ "@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/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.34.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-hp7rV0a0gfD/rNw9et+PqM1PPkgFC6/Z7eYfza6NIp/m+a8/r5Um+S/8tzBDCgZmQC9Y1sJsjesH7mXZz6Jmuw=="],
diff --git a/package.json b/package.json
index 7f969e8..d87ae5a 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
},
"overrides": {
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz",
- "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz"
+ "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz"
},
"dependencies": {
"@tensamin/ttp-core": "*",
diff --git a/packages/call/src/components/buttons/screenshare.tsx b/packages/call/src/components/buttons/screenshare.tsx
index 4471539..21ba118 100644
--- a/packages/call/src/components/buttons/screenshare.tsx
+++ b/packages/call/src/components/buttons/screenshare.tsx
@@ -18,10 +18,12 @@ export default function ScreenshareButton({
className,
iconSize,
tooltip,
+ defaultPortal,
}: {
className?: string;
iconSize?: number;
tooltip?: string;
+ defaultPortal?: boolean;
}) {
const isScreensharing = useCall((state) => state.screenShareEnabled);
const screenRef = useCall((state) => state.screenRef);
@@ -30,8 +32,9 @@ export default function ScreenshareButton({
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
+ if (defaultPortal) return;
setPortalContainer(screenRef?.current ?? undefined);
- }, [screenRef]);
+ }, [screenRef, defaultPortal]);
async function startWebShare() {
try {
@@ -110,7 +113,9 @@ export default function ScreenshareButton({
/>
{/* Detect video / user and place here */}
diff --git a/packages/call/src/components/sidebarBox.tsx b/packages/call/src/components/sidebarBox.tsx
index 294d051..6a850f0 100644
--- a/packages/call/src/components/sidebarBox.tsx
+++ b/packages/call/src/components/sidebarBox.tsx
@@ -20,24 +20,18 @@ import LeaveButton from "./buttons/leave";
export default function SidebarBox() {
const state = useCall((store) => store.state);
- const screenRef = useCall((store) => store.screenRef);
const isMobile = useIsMobile();
- const [portalContainer, setPortalContainer] = useState
();
-
- useEffect(() => {
- setPortalContainer(screenRef?.current ?? undefined);
- }, [screenRef]);
return state === "closed" ? null : (
-
+
-
+
@@ -45,7 +39,7 @@ export default function SidebarBox() {
);
}
-function ConnectionBar({ portalContainer }: { portalContainer?: HTMLElement }) {
+function ConnectionBar() {
const state = useCall((store) => store.state);
const isEncrypted = useCall((store) => store.isEncrypted);
const callId = useCall((store) => store.callId);
@@ -68,7 +62,7 @@ function ConnectionBar({ portalContainer }: { portalContainer?: HTMLElement }) {
{state === "closed" && "Closed"}
{state === "closing" && "Closing"}
-
+
{isEncrypted ? (
@@ -78,18 +72,12 @@ function ConnectionBar({ portalContainer }: { portalContainer?: HTMLElement }) {
}
/>
-
- Click to open call page
-
+ Click to open call page
);
}
-export function TinyPingGraph({
- portalContainer,
-}: {
- portalContainer?: HTMLElement;
-}) {
+export function TinyPingGraph() {
const room = useCall((store) => store.room);
const [mapData, setMapData] = useState
}
/>
-