feat: redesign + avatars + theme + call presence fixes (v0.6.0)
Release desktop app / build (, windows-latest) (push) Has been cancelled
Release desktop app / build (--target universal-apple-darwin --bundles app,updater, macos-14) (push) Has been cancelled

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:
2026-04-20 00:26:19 +02:00
parent 0ca29952ba
commit 4db65993d5
33 changed files with 2459 additions and 1094 deletions
@@ -0,0 +1,170 @@
import { useTranslation } from 'react-i18next';
import {
MicIcon,
MicOffIcon,
MonitorShareIcon,
MonitorStopIcon,
PhoneOffIcon,
UsersIcon,
VideoIcon,
} from './icons';
interface Props {
muted: boolean;
sharing: boolean;
video: boolean;
onToggleMute: () => void;
onToggleShare: () => void;
onToggleVideo?: () => void;
onHangup: () => void;
onOpenParticipants?: () => void;
/** Compact variant used inside the docked call (36px buttons). */
compact?: boolean;
/** Glass variant used when controls float on fullscreen cinema mode. */
glass?: boolean;
/** Disable transient interactions while the call isn't fully connected. */
disabledMedia?: boolean;
}
export function CallControls({
muted,
sharing,
video,
onToggleMute,
onToggleShare,
onToggleVideo,
onHangup,
onOpenParticipants,
compact = false,
glass = false,
disabledMedia = false,
}: Props) {
const { t } = useTranslation(['app']);
const wrapBase = glass
? 'glass-pill flex items-center justify-center gap-2 rounded-[18px] p-2.5'
: 'flex items-center justify-center gap-2 border-t border-line bg-surface-3 p-3';
const btnSize = compact ? 'h-9 w-9 rounded-[10px]' : 'h-12 w-12 rounded-[14px]';
const hangupSize = compact ? 'h-9 w-[54px] rounded-[10px]' : 'h-12 w-[72px] rounded-[14px]';
return (
<div className={wrapBase}>
<CallButton
label={muted ? t('app:call.unmute') : t('app:call.mute')}
active={muted}
activeTone="danger"
onClick={onToggleMute}
disabled={disabledMedia}
glass={glass}
className={btnSize}
>
{muted ? <MicOffIcon className="h-5 w-5" /> : <MicIcon className="h-5 w-5" />}
</CallButton>
{onToggleVideo && (
<CallButton
label={t('app:call.start_video', { defaultValue: 'Video' })}
active={video}
activeTone="accent"
onClick={onToggleVideo}
disabled={disabledMedia}
glass={glass}
className={btnSize}
>
<VideoIcon className="h-5 w-5" />
</CallButton>
)}
<CallButton
label={
sharing
? t('app:call.stop_share_screen', { defaultValue: 'Screen-Share stoppen' })
: t('app:call.share_screen', { defaultValue: 'Bildschirm teilen' })
}
active={sharing}
activeTone="accent"
onClick={onToggleShare}
disabled={disabledMedia}
glass={glass}
className={btnSize}
>
{sharing ? (
<MonitorStopIcon className="h-5 w-5" />
) : (
<MonitorShareIcon className="h-5 w-5" />
)}
</CallButton>
{onOpenParticipants && (
<CallButton
label={t('app:call.participants', { defaultValue: 'Teilnehmer' })}
onClick={onOpenParticipants}
glass={glass}
className={btnSize}
>
<UsersIcon className="h-5 w-5" />
</CallButton>
)}
<CallButton
label={t('app:call.hangup')}
onClick={onHangup}
tone="danger"
glass={glass}
className={hangupSize}
>
<PhoneOffIcon className="h-5 w-5" />
</CallButton>
</div>
);
}
interface CallButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
active?: boolean;
activeTone?: 'accent' | 'danger';
tone?: 'default' | 'danger';
glass?: boolean;
className?: string;
children: React.ReactNode;
}
function CallButton({
label,
onClick,
disabled,
active,
activeTone = 'accent',
tone = 'default',
glass = false,
className = '',
children,
}: CallButtonProps) {
const base =
'inline-flex cursor-pointer items-center justify-center border transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-not-allowed disabled:opacity-50';
let toneClass: string;
if (tone === 'danger') {
toneClass = 'bg-rose-600 text-white border-rose-600 hover:bg-rose-500';
} else if (active && activeTone === 'danger') {
toneClass = 'bg-rose-600 text-white border-rose-600 hover:bg-rose-500';
} else if (active) {
toneClass = 'bg-accent text-accent-fg border-accent hover:brightness-110';
} else if (glass) {
toneClass =
'border-white/15 bg-white/10 text-white hover:bg-white/15';
} else {
toneClass = 'border-line bg-surface-2 text-fg hover:bg-surface';
}
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-label={label}
aria-pressed={active}
title={label}
className={`${base} ${toneClass} ${className}`}
>
{children}
</button>
);
}