feat: redesign + avatars + theme + call presence fixes (v0.6.0)
Visual:
- Light/dark theme via ThemeContext + CSS vars
- New design tokens (surface/fg/accent/line/etc.) across all pages
- Reusable Avatar component (img + letter fallback) wired into UserBar,
ConversationHeader, ChatsPage, FriendsPage, GroupInfoPanel, IncomingCallPanel
Calls:
- Split call UI: IncomingCallPanel, InCallPanel, CallControls,
CallParticipantTile, ScreenShareViewer
- Active speaker hook (useActiveSpeakers)
- Fix: ActiveCallBanner stayed hidden after hangup while peers in room.
- useCallPresence: bind presence callbacks only when we own subscribe
(Supabase forbids .on() after .subscribe() on shared dedup'd channels)
- useCallPresence: never removeChannel — channel is shared with CallContext
so tearing it down on ConversationHeader unmount killed live tracking
- ActiveCallBanner: lastCallConversationId fallback so banner shows
instantly after hangup, auto-dismiss when room confirmed empty
- Drop unused useAnyActiveCall
Bump tauri version 0.5.0 -> 0.6.0
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import type { ConversationSummary } from '@chat-app/shared/chat';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useCall } from '../context/CallContext';
|
||||
import { useFriendshipsContext } from '../context/FriendshipsContext';
|
||||
import { AvatarColorKey, colorKeyFor } from './CallParticipantTile';
|
||||
import { LockIcon, PhoneIcon, PhoneOffIcon } from './icons';
|
||||
|
||||
interface Props {
|
||||
conversation: ConversationSummary;
|
||||
}
|
||||
|
||||
const PULSE_BG: Record<AvatarColorKey, string> = {
|
||||
violet: 'bg-violet-200 text-violet-800 dark:bg-violet-900/60 dark:text-violet-200',
|
||||
amber: 'bg-amber-200 text-amber-900 dark:bg-amber-900/60 dark:text-amber-200',
|
||||
rose: 'bg-rose-200 text-rose-900 dark:bg-rose-900/60 dark:text-rose-200',
|
||||
teal: 'bg-teal-200 text-teal-900 dark:bg-teal-900/60 dark:text-teal-200',
|
||||
};
|
||||
|
||||
// Docked incoming-call panel: replaces the chat header, chat + composer stay
|
||||
// visible and usable below (per user decision). Full-bleed overlay lives in
|
||||
// the shell-level toast for cross-conversation ring notifications.
|
||||
export function IncomingCallPanel({ conversation }: Props) {
|
||||
const { t } = useTranslation(['app']);
|
||||
const { state, acceptIncoming, rejectIncoming } = useCall();
|
||||
const { friendships } = useFriendshipsContext();
|
||||
|
||||
if (state.kind !== 'incoming' || state.conversationId !== conversation.id) return null;
|
||||
|
||||
const isGroup = conversation.type === 'group';
|
||||
const callerProfile = conversation.members.find((m) => m.userId === state.fromUserId)?.profile ?? null;
|
||||
const callerName =
|
||||
callerProfile?.displayName ??
|
||||
friendships.find((f) => f.peer.userId === state.fromUserId)?.peer.displayName ??
|
||||
'?';
|
||||
const groupName = isGroup ? (conversation.name ?? t('app:chats.new_group')) : null;
|
||||
const title = isGroup ? groupName ?? callerName : callerName;
|
||||
const letter = title.trim().charAt(0).toUpperCase() || '?';
|
||||
const avatarUrl = isGroup
|
||||
? (conversation.avatarUrl ?? callerProfile?.avatarUrl ?? null)
|
||||
: callerProfile?.avatarUrl ?? null;
|
||||
const colorKey = colorKeyFor(state.fromUserId);
|
||||
const avatarTone = PULSE_BG[colorKey];
|
||||
const mediaLabel =
|
||||
state.mediaKind === 'video' ? 'Video Call' : 'Voice Call';
|
||||
|
||||
return (
|
||||
<section
|
||||
role="region"
|
||||
aria-label={t('app:call.incoming_title')}
|
||||
className="relative overflow-hidden border-b border-line bg-surface-3"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(79,70,229,0.10),transparent_60%),radial-gradient(circle_at_70%_80%,rgba(109,115,255,0.08),transparent_55%)]" />
|
||||
<div className="relative mx-auto flex max-w-[480px] flex-col items-center px-6 py-8 text-center">
|
||||
<p className="text-[12px] font-semibold uppercase tracking-[0.12em] text-fg-muted">
|
||||
{t('app:call.incoming_title')}
|
||||
</p>
|
||||
<div className="relative my-4 flex h-[112px] w-[112px] items-center justify-center">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 rounded-full border-2 border-accent animate-pulse-ring"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 rounded-full border-2 border-accent animate-pulse-ring"
|
||||
style={{ animationDelay: '1s' }}
|
||||
/>
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={title}
|
||||
className="h-24 w-24 rounded-full border-[3px] border-surface-3 object-cover"
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
'flex h-24 w-24 items-center justify-center rounded-full border-[3px] border-surface-3 font-display text-4xl font-bold ' +
|
||||
avatarTone
|
||||
}
|
||||
>
|
||||
{letter}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="font-display text-2xl font-bold tracking-tight text-fg">{title}</p>
|
||||
<p className="mt-1.5 flex items-center gap-1.5 text-xs text-fg-muted">
|
||||
<LockIcon className="h-3.5 w-3.5" />
|
||||
<span>E2E verschlüsselt · {mediaLabel}</span>
|
||||
{isGroup && (
|
||||
<>
|
||||
<span className="text-fg-muted/50" aria-hidden="true">·</span>
|
||||
<span>
|
||||
{t('app:call.incoming_group_from', {
|
||||
name: callerName,
|
||||
defaultValue: callerName + ' ruft an',
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-5 flex w-full max-w-[360px] gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={rejectIncoming}
|
||||
className="inline-flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-[14px] border border-rose-500/40 bg-transparent px-5 py-3.5 text-sm font-semibold text-rose-600 transition hover:bg-rose-500/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/40 dark:text-rose-400"
|
||||
>
|
||||
<PhoneOffIcon className="h-4 w-4" />
|
||||
<span>{t('app:call.decline')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void acceptIncoming()}
|
||||
className="inline-flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-[14px] bg-emerald-600 px-5 py-3.5 text-sm font-semibold text-white shadow-accept-btn transition hover:bg-emerald-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/50"
|
||||
>
|
||||
<PhoneIcon className="h-4 w-4" />
|
||||
<span>{t('app:call.accept')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user