(feat): improve profile popup in the navbar, add shared code to that popup
All checks were successful
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m18s
/ build-mobile (push) Successful in 12m28s
/ release (push) Successful in 29s

This commit is contained in:
Alois 2026-06-03 16:14:39 +02:00
commit f4088691c5
4 changed files with 114 additions and 14 deletions

View file

@ -1,8 +1,62 @@
import type { User } from "@tensamin/user/context"; 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 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 }) { 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 ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@ -18,17 +72,42 @@ export default function Profile({ user }: { user: User }) {
</div> </div>
</div> </div>
<Text value={user.about || ""} /> <Text value={user.about || ""} />
<div className="flex flex-col"> <Button
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap"> className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
iota: {user.iota_id} variant="link"
</p> aria-expanded={showAdvancedInformation}
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap"> onClick={() => setShowAdvancedInformation((show) => !show)}
user: {user.user_id} >
</p> {showAdvancedInformation ? (
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap"> <ChevronUp className="size-4" />
pub key: {user.public_key} ) : (
</p> <ChevronDown className="size-4" />
</div> )}
<span>Show advanced information</span>
</Button>
{showAdvancedInformation && (
<div className="flex flex-col">
<p className="text-white flex gap-1 overflow-hidden text-ellipsis whitespace-nowrap">
Shared Code: <code>{sharedSecretCode}</code>{" "}
<Tooltip>
<TooltipTrigger render={<Info className="size-3.5" />} />
<TooltipContent>
You can compare this code with the conversation partner to
validate that this chat is E2EE
</TooltipContent>
</Tooltip>
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
Iota ID: <code>{user.iota_id}</code>
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
User ID: <code>{user.user_id}</code>
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
Public Key: <code>{user.public_key}</code>
</p>
</div>
)}
</div> </div>
); );
} }

View file

@ -74,7 +74,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
userId={id} userId={id}
component={(user) => component={(user) =>
isMobile ? ( isMobile ? (
<p className="font-medium text-md">{user?.display}</p> <p className="font-medium text-[1.07rem]">{user?.display}</p>
) : ( ) : (
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}> <Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
<PopoverTrigger <PopoverTrigger
@ -86,7 +86,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
textDecorationLine: "none", textDecorationLine: "none",
}} }}
> >
<p className="font-medium text-md">{user?.display}</p> <p className="font-medium text-[1.07rem]">{user?.display}</p>
</Button> </Button>
} }
/> />

View file

@ -4,6 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"exports": { "exports": {
"./code": "./src/code.ts",
"./data": "./src/data.ts", "./data": "./src/data.ts",
"./desktopMedia": "./src/desktopMedia.tsx", "./desktopMedia": "./src/desktopMedia.tsx",
"./log": "./src/log.tsx", "./log": "./src/log.tsx",

View file

@ -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");
}