import type { Component } from "solid-js"; import { Show, createMemo, createEffect, onCleanup } from "solid-js"; import { Portal } from "solid-js/web"; import { UserPlus, UserMinus, Copy, Check } from "lucide-solid"; import { createSignal } from "solid-js"; import Avatar from "./Avatar"; import { profileCardTarget, closeProfileCard, openProfileModal, } from "../../stores/ui"; import { members, isPeerOnline } from "../../stores/members"; import { knownPeers } from "../../stores/directory"; import { identity } from "../../stores/identity"; import { settings } from "../../stores/settings"; import { markAsFriend, unmarkAsFriend } from "../../stores/directory"; import * as tauri from "../../lib/tauri"; import { formatTime } from "../../lib/utils"; const ProfileCard: Component = () => { const [copied, setCopied] = createSignal(false); let cardRef: HTMLDivElement | undefined; const target = () => profileCardTarget(); const isOpen = () => target() !== null; const isSelf = () => target()?.peerId === identity()?.peer_id; // pull rich info from member list or directory const memberInfo = createMemo(() => { const t = target(); if (!t) return null; return members().find((m) => m.peer_id === t.peerId) ?? null; }); const directoryInfo = createMemo(() => { const t = target(); if (!t) return null; return knownPeers().find((p) => p.peer_id === t.peerId) ?? null; }); const displayName = () => memberInfo()?.display_name ?? directoryInfo()?.display_name ?? target()?.displayName ?? (isSelf() ? identity()?.display_name : null) ?? "Unknown"; const bio = () => directoryInfo()?.bio || (isSelf() ? identity()?.bio : "") || ""; const isFriend = () => directoryInfo()?.is_friend ?? false; // local user always knows their own status from settings; // remote peers use member list status or online tracking as fallback const status = () => { if (isSelf()) { const s = settings().status; if (s === "invisible") return "Offline"; return (s.charAt(0).toUpperCase() + s.slice(1)) as | "Online" | "Idle" | "Dnd" | "Offline"; } const member = memberInfo(); if (member) return member.status; const t = target(); if (t && isPeerOnline(t.peerId)) return "Online"; return "Offline"; }; const roles = () => memberInfo()?.roles ?? []; const joinedAt = () => memberInfo()?.joined_at ?? 0; // close on escape or click outside function handleKeydown(e: KeyboardEvent) { if (e.key === "Escape") closeProfileCard(); } function handleClickOutside(e: MouseEvent) { if (cardRef && !cardRef.contains(e.target as Node)) { closeProfileCard(); } } createEffect(() => { if (isOpen()) { // delay listener registration to avoid the triggering click from closing it requestAnimationFrame(() => { document.addEventListener("mousedown", handleClickOutside); }); document.addEventListener("keydown", handleKeydown); } }); onCleanup(() => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleKeydown); }); // close and re-register listeners whenever the target changes createEffect(() => { if (!isOpen()) { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleKeydown); } }); // compute position to stay within viewport const cardPosition = createMemo(() => { const t = target(); if (!t) return { top: 0, left: 0 }; const cardWidth = 320; const cardHeight = 340; const margin = 12; const vw = window.innerWidth; const vh = window.innerHeight; let left = t.anchorX + margin; let top = t.anchorY - cardHeight / 3; // flip horizontally if overflowing right if (left + cardWidth > vw - margin) { left = t.anchorX - cardWidth - margin; } // clamp vertically if (top < margin) top = margin; if (top + cardHeight > vh - margin) top = vh - cardHeight - margin; return { top, left }; }); async function handleToggleFriend() { const t = target(); if (!t) return; try { if (isFriend()) { await tauri.removeFriend(t.peerId); unmarkAsFriend(t.peerId); } else { await tauri.addFriend(t.peerId); markAsFriend(t.peerId); } } catch (e) { console.error("failed to toggle friend:", e); } } async function handleCopyPeerId() { const t = target(); if (!t) return; try { await navigator.clipboard.writeText(t.peerId); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { // clipboard api may fail outside secure contexts } } function handleOpenFullProfile() { const t = target(); if (t) openProfileModal(t.peerId); } const statusLabel = () => { const s = status(); if (s === "Online") return "online"; if (s === "Idle") return "idle"; return "offline"; }; const statusColor = () => { const s = status(); if (s === "Online") return "bg-success"; if (s === "Idle") return "bg-warning"; return "bg-gray-300"; }; return (
{/* header banner */}
{/* avatar overlapping the banner */}
{/* user info */}
(you)
{/* status indicator */}
{statusLabel()}
{/* bio */}

{bio()}

{/* metadata section */}
{/* roles */} 0}>
roles
{roles().map((role) => ( {role} ))}
{/* joined date */} 0}>
member since

{formatTime(joinedAt())}

{/* peer id */}
peer id
{/* action buttons */}
); }; export default ProfileCard;