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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user