feat: backup/restore, user profile popover, image compress, video blur, wake lock
- 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
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user