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
+41 -18
View File
@@ -7,9 +7,11 @@ import { ConversationHeader } from '../components/ConversationHeader';
import { GroupInfoPanel } from '../components/GroupInfoPanel';
import { AlertIcon, ArrowRightIcon, PlusIcon, SpinnerIcon, XIcon } from '../components/icons';
import { InCallPanel } from '../components/InCallPanel';
import { IncomingCallPanel } from '../components/IncomingCallPanel';
import { MessageBubble } from '../components/MessageBubble';
import { TypingIndicator } from '../components/TypingIndicator';
import { useAuth } from '../context/AuthContext';
import { useCall } from '../context/CallContext';
import { useConversationsContext } from '../context/ConversationsContext';
import { useConversationMessages } from '../lib/useConversationMessages';
import { useMessageReactions } from '../lib/useMessageReactions';
@@ -157,14 +159,27 @@ export function ConversationPage() {
}
const isGroup = conversation?.type === 'group';
const { state: callState } = useCall();
// Hide the chat header while this conversation hosts an active call — the
// call topbar inside the dock already shows the channel name + duration,
// and fullscreen cinema needs the whole slot.
const callHereActive =
(callState.kind === 'connected' ||
callState.kind === 'connecting' ||
callState.kind === 'outgoing') &&
callState.conversationId === id;
const incomingHere =
callState.kind === 'incoming' && callState.conversationId === id;
return (
<div className="relative flex h-full flex-col">
<ConversationHeader
conversation={conversation}
peerPresence={peerPresence}
{...(isGroup ? { onInfoClick: () => setInfoPanelOpen(true) } : {})}
/>
{!callHereActive && (
<ConversationHeader
conversation={conversation}
peerPresence={peerPresence}
{...(isGroup ? { onInfoClick: () => setInfoPanelOpen(true) } : {})}
/>
)}
{isGroup && conversation && (
<GroupInfoPanel
@@ -174,28 +189,36 @@ export function ConversationPage() {
/>
)}
{incomingHere && conversation && <IncomingCallPanel conversation={conversation} />}
{conversation && <InCallPanel conversation={conversation} />}
<div ref={scrollRef} onScroll={handleScroll} className="flex-1 overflow-y-auto px-6 py-4">
<div ref={scrollRef} onScroll={handleScroll} className="flex-1 overflow-y-auto bg-surface-3 px-6 py-4">
{loading ? (
<div className="flex items-center gap-2 text-xs text-neutral-500">
<SpinnerIcon className="h-3.5 w-3.5 text-brand-400" />
<div className="flex items-center gap-2 text-xs text-fg-muted">
<SpinnerIcon className="h-3.5 w-3.5 text-accent" />
</div>
) : error ? (
<Banner>{error}</Banner>
) : messages.length === 0 ? (
<p className="text-center text-sm text-neutral-500"></p>
<p className="text-center text-sm text-fg-muted"></p>
) : (
<ul className="space-y-0.5">
{messages.map((m, idx) => {
const prev = messages[idx - 1];
const next = messages[idx + 1];
const grouped = idx > 0 && prev?.senderId === m.senderId;
const isLastOfRun = !next || next.senderId !== m.senderId;
const senderProfile =
conversation?.members.find((mm) => mm.userId === m.senderId)?.profile ?? null;
return (
<li key={m.id}>
<MessageBubble
message={m}
mine={m.senderId === myId}
groupedWithPrev={grouped}
isLastOfRun={isLastOfRun}
senderDisplayName={senderProfile?.displayName}
senderAvatarUrl={senderProfile?.avatarUrl}
conversationId={id ?? ''}
reactions={reactionsByMessage.get(m.id) ?? []}
onToggleReaction={(emoji) => toggleReaction(m.id, emoji)}
@@ -213,7 +236,7 @@ export function ConversationPage() {
members={conversation?.members ?? []}
/>
<form onSubmit={handleSend} className="border-t border-white/5 p-4">
<form onSubmit={handleSend} className="border-t border-line bg-surface-3 p-4">
{sendError && (
<div className="mb-2">
<Banner>{sendError}</Banner>
@@ -248,7 +271,7 @@ export function ConversationPage() {
onClick={() => fileInputRef.current?.click()}
aria-label="Bild anhängen"
title="Bild anhängen"
className="inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-lg border border-white/10 bg-white/5 text-neutral-300 transition hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400/40"
className="inline-flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-accent text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"
>
<PlusIcon className="h-4 w-4" />
</button>
@@ -265,14 +288,14 @@ export function ConversationPage() {
}
}}
rows={1}
placeholder="…"
className="max-h-40 min-h-[44px] flex-1 resize-none rounded-lg border border-white/10 bg-ink-900/60 px-3 py-2.5 text-sm text-white placeholder-neutral-500 transition focus:border-brand-400 focus:outline-none focus:ring-2 focus:ring-brand-500/40"
placeholder="Nachricht schreiben…"
className="max-h-40 min-h-[44px] flex-1 resize-none rounded-lg border border-line bg-surface-2 px-3 py-2.5 text-sm text-fg placeholder-fg-muted transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
/>
<button
type="submit"
disabled={sending || (text.trim().length === 0 && attachments.length === 0)}
aria-busy={sending}
className="inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-lg bg-gradient-to-br from-brand-400 to-brand-600 text-white transition hover:from-brand-300 hover:to-brand-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400/60 disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex h-11 w-11 shrink-0 cursor-pointer items-center justify-center rounded-lg bg-accent text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/60 disabled:cursor-not-allowed disabled:opacity-60"
>
{sending ? <SpinnerIcon className="h-4 w-4" /> : <ArrowRightIcon className="h-4 w-4" />}
</button>
@@ -286,9 +309,9 @@ function Banner({ children }: { children: React.ReactNode }) {
return (
<div
role="alert"
className="flex items-start gap-3 rounded-lg border border-rose-500/20 bg-rose-500/10 p-3 text-sm text-rose-100"
className="flex items-start gap-3 rounded-lg border border-rose-500/30 bg-rose-500/10 p-3 text-sm text-rose-700 dark:text-rose-100"
>
<AlertIcon className="mt-0.5 h-5 w-5 shrink-0 text-rose-400" />
<AlertIcon className="mt-0.5 h-5 w-5 shrink-0 text-rose-500 dark:text-rose-400" />
<p className="min-w-0 flex-1 break-words">{children}</p>
</div>
);
@@ -302,7 +325,7 @@ function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => voi
return () => URL.revokeObjectURL(u);
}, [file]);
return (
<div className="relative overflow-hidden rounded-lg border border-white/10 bg-ink-900/60">
<div className="relative overflow-hidden rounded-lg border border-line bg-surface-2">
{url ? (
<img src={url} alt={file.name} className="block h-20 w-20 object-cover" />
) : (
@@ -312,7 +335,7 @@ function AttachmentPreview({ file, onRemove }: { file: File; onRemove: () => voi
type="button"
onClick={onRemove}
aria-label="Entfernen"
className="absolute right-1 top-1 flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-ink-950/80 text-neutral-200 transition hover:bg-rose-500/70 hover:text-white"
className="absolute right-1 top-1 flex h-5 w-5 cursor-pointer items-center justify-center rounded-full bg-black/70 text-white transition hover:bg-rose-500/80"
>
<XIcon className="h-3 w-3" />
</button>