1303c8e26f
- backup/restore dialog + user profile popover components - image compression, video blur, wake lock utilities - message cache + conversation messages hook refinements - call context, active speakers, screen share dialog tweaks - audio + screen share settings persistence - refreshed app icons (smaller sizes) across all platforms
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import type { ProfileBrief } from '@chat-app/shared/friends';
|
|
import { useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { usePeerPresence } from '../lib/usePeerPresence';
|
|
import { Avatar } from './Avatar';
|
|
|
|
interface Props {
|
|
userId: string;
|
|
profile: ProfileBrief | null;
|
|
x: number;
|
|
y: number;
|
|
onClose: () => void;
|
|
onStartDm?: (userId: string) => void;
|
|
}
|
|
|
|
const PRESENCE_DOT: Record<string, string> = {
|
|
online: 'bg-emerald-500',
|
|
idle: 'bg-amber-400',
|
|
dnd: 'bg-rose-500',
|
|
invisible: 'bg-neutral-500',
|
|
offline: 'bg-neutral-400 dark:bg-neutral-600',
|
|
};
|
|
|
|
const CARD_W = 280;
|
|
const CARD_H = 180;
|
|
|
|
// Hover/click card surfaced from avatars around the app. Shows display name,
|
|
// @handle, presence state + status message, plus a DM-start button when
|
|
// the clicked profile isn't the caller.
|
|
export function UserProfilePopover({
|
|
userId,
|
|
profile,
|
|
x,
|
|
y,
|
|
onClose,
|
|
onStartDm,
|
|
}: Props) {
|
|
// Subscribe to the same presence feed that the conversation header uses,
|
|
// so status updates flow in live.
|
|
const presence = usePeerPresence(userId);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
const onDown = (e: MouseEvent) => {
|
|
const t = e.target as HTMLElement | null;
|
|
if (t?.closest('[data-user-popover]')) return;
|
|
if (t?.closest('[data-user-popover-trigger]')) return;
|
|
onClose();
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
window.addEventListener('mousedown', onDown);
|
|
return () => {
|
|
window.removeEventListener('keydown', onKey);
|
|
window.removeEventListener('mousedown', onDown);
|
|
};
|
|
}, [onClose]);
|
|
|
|
const left = Math.min(Math.max(8, x), window.innerWidth - CARD_W - 8);
|
|
const top = Math.min(Math.max(8, y), window.innerHeight - CARD_H - 8);
|
|
|
|
const displayName = profile?.displayName ?? '?';
|
|
const username = profile?.username ?? '';
|
|
const state = presence?.state ?? 'offline';
|
|
const showPresence = state !== 'invisible';
|
|
const statusMessage = presence?.statusMessage?.trim() ?? '';
|
|
|
|
return createPortal(
|
|
<div
|
|
data-user-popover
|
|
role="dialog"
|
|
aria-label={displayName}
|
|
style={{ left, top, width: CARD_W }}
|
|
className="fixed z-[80] flex flex-col gap-3 rounded-xl border border-line bg-surface-2/95 p-4 shadow-xl backdrop-blur-md"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="relative">
|
|
<Avatar
|
|
url={profile?.avatarUrl ?? null}
|
|
displayName={displayName}
|
|
className="h-14 w-14 text-lg"
|
|
/>
|
|
{showPresence && (
|
|
<span
|
|
aria-hidden="true"
|
|
className={
|
|
'absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full ring-2 ring-surface-2 ' +
|
|
(PRESENCE_DOT[state] ?? PRESENCE_DOT.offline)
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate font-display text-base font-semibold text-fg">
|
|
{displayName}
|
|
</p>
|
|
{username && (
|
|
<p className="truncate text-xs text-fg-muted">@{username}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{statusMessage && state !== 'offline' && (
|
|
<p className="rounded-md bg-surface-3 px-2.5 py-1.5 text-xs italic text-fg-muted">
|
|
{statusMessage}
|
|
</p>
|
|
)}
|
|
|
|
{onStartDm && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onStartDm(userId);
|
|
onClose();
|
|
}}
|
|
className="inline-flex cursor-pointer items-center justify-center rounded-md bg-accent px-3 py-1.5 text-xs font-semibold text-accent-fg transition hover:brightness-110"
|
|
>
|
|
Nachricht senden
|
|
</button>
|
|
)}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|