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
+66 -27
View File
@@ -14,6 +14,7 @@ import { devLocalSecretStore } from '../lib/secretStore';
import { supabase } from '../lib/supabase';
import type { AggregatedReaction } from '../lib/useMessageReactions';
import { AttachmentImage } from './AttachmentImage';
import { Avatar } from './Avatar';
import { PencilIcon, PhoneIcon, PhoneOffIcon, SmileIcon, SpinnerIcon, TrashIcon, XIcon } from './icons';
const EMOJI_CHOICES = ['👍', '❤️', '😂', '🎉', '🔥', '😮', '😢', '🙏'];
@@ -23,6 +24,10 @@ interface Props {
message: DecryptedMessage;
mine: boolean;
groupedWithPrev: boolean;
/** Last message of a run from this sender — anchor avatar on this row. */
isLastOfRun?: boolean;
senderDisplayName?: string | null | undefined;
senderAvatarUrl?: string | null | undefined;
conversationId: string;
reactions: AggregatedReaction[];
onToggleReaction: (emoji: string) => Promise<void>;
@@ -33,6 +38,9 @@ export function MessageBubble({
message,
mine,
groupedWithPrev,
isLastOfRun = false,
senderDisplayName,
senderAvatarUrl,
conversationId,
reactions,
onToggleReaction,
@@ -143,8 +151,13 @@ export function MessageBubble({
if (message.deletedAt) {
return (
<div className={'flex ' + (mine ? 'justify-end' : 'justify-start')}>
<div className="max-w-[70%] rounded-2xl border border-white/5 bg-white/5 px-3.5 py-1.5 text-xs italic text-neutral-500">
<div className={'flex items-end gap-2 ' + (mine ? 'flex-row-reverse' : '')}>
<AvatarSlot
show={isLastOfRun}
url={senderAvatarUrl ?? null}
displayName={senderDisplayName ?? null}
/>
<div className="max-w-[calc(70%-2.5rem)] rounded-2xl border border-line bg-surface-2 px-3.5 py-1.5 text-xs italic text-fg-muted">
{t('app:chats.deleted')}
</div>
</div>
@@ -156,14 +169,19 @@ export function MessageBubble({
}
return (
<div className={'flex ' + (mine ? 'justify-end' : 'justify-start')}>
<div className={'flex items-end gap-2 ' + (mine ? 'flex-row-reverse' : '')}>
<AvatarSlot
show={isLastOfRun}
url={senderAvatarUrl ?? null}
displayName={senderDisplayName ?? null}
/>
<div
className={
'group relative max-w-[70%] ' + (groupedWithPrev ? 'mt-0.5' : 'mt-2')
'group relative max-w-[calc(70%-2.5rem)] ' + (groupedWithPrev ? 'mt-0.5' : 'mt-2')
}
>
{editing ? (
<div className="rounded-2xl border border-brand-400/40 bg-ink-900/80 p-2 backdrop-blur-xl">
<div className="rounded-2xl border border-accent/40 bg-surface-2 p-2 shadow-sm">
<textarea
value={editText}
onChange={(e) => setEditText(e.target.value)}
@@ -179,10 +197,10 @@ export function MessageBubble({
}}
rows={2}
autoFocus
className="w-full resize-none rounded-md bg-ink-800 px-3 py-2 text-sm text-white outline-none focus:ring-2 focus:ring-brand-400/60"
className="w-full resize-none rounded-md border border-line bg-surface-3 px-3 py-2 text-sm text-fg outline-none focus:ring-2 focus:ring-accent/50"
/>
{editError && (
<p className="mt-1.5 break-words rounded-md border border-rose-500/20 bg-rose-500/10 px-2 py-1 text-[11px] text-rose-200">
<p className="mt-1.5 break-words rounded-md border border-rose-500/30 bg-rose-500/10 px-2 py-1 text-[11px] text-rose-600 dark:text-rose-200">
{editError}
</p>
)}
@@ -193,7 +211,7 @@ export function MessageBubble({
setEditing(false);
setEditError(null);
}}
className="cursor-pointer rounded-md border border-white/10 bg-white/5 px-3 py-1 text-xs text-neutral-200 hover:bg-white/10"
className="cursor-pointer rounded-md border border-line bg-surface-3 px-3 py-1 text-xs text-fg hover:bg-surface-2"
>
{t('app:friends.action_cancel')}
</button>
@@ -201,7 +219,7 @@ export function MessageBubble({
type="button"
disabled={busy}
onClick={() => void handleEditSave()}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-brand-500/90 px-3 py-1 text-xs font-semibold text-white hover:bg-brand-400 disabled:opacity-60"
className="inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-accent px-3 py-1 text-xs font-semibold text-accent-fg hover:brightness-110 disabled:opacity-60"
>
{busy && <SpinnerIcon className="h-3.5 w-3.5" />}
<span>{t('common:save', { defaultValue: 'Save' })}</span>
@@ -211,14 +229,14 @@ export function MessageBubble({
) : (
<div
className={
'break-words rounded-2xl px-3.5 py-2 text-sm ' +
'break-words px-3.5 py-2 text-sm ' +
(mine
? 'bg-brand-500/85 text-white'
: 'border border-white/5 bg-ink-900/70 text-neutral-100')
? 'rounded-[18px_18px_4px_18px] bg-accent text-accent-fg'
: 'rounded-[18px_18px_18px_4px] border border-line text-fg')
}
>
{message.plaintext === null ? (
<span className="italic text-neutral-400">cannot decrypt</span>
<span className="italic opacity-70">cannot decrypt</span>
) : (
<>
{bodyText.length > 0 && <div>{bodyText}</div>}
@@ -230,7 +248,7 @@ export function MessageBubble({
<div
className={
'mt-1 flex items-center gap-1.5 text-[10px] ' +
(mine ? 'text-brand-100/70' : 'text-neutral-500')
(mine ? 'text-accent-fg/75' : 'text-fg-muted')
}
>
<span>{time}</span>
@@ -242,7 +260,7 @@ export function MessageBubble({
)}
{showSeen && mine && !editing && !message.deletedAt && (
<p className="mt-0.5 text-right text-[10px] text-neutral-500">
<p className="mt-0.5 text-right text-[10px] text-fg-muted">
{t('app:chats.seen')}
</p>
)}
@@ -255,10 +273,10 @@ export function MessageBubble({
type="button"
onClick={() => void onToggleReaction(r.emoji)}
className={
'inline-flex cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400/40 ' +
'inline-flex cursor-pointer items-center gap-1 rounded-full border px-2 py-0.5 text-xs transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
(r.mine
? 'border-brand-400/40 bg-brand-500/20 text-brand-100'
: 'border-white/10 bg-white/5 text-neutral-200 hover:bg-white/10')
? 'border-accent/40 bg-accent/20 text-accent'
: 'border-line bg-surface-2 text-fg hover:bg-surface-3')
}
>
<span>{r.emoji}</span>
@@ -275,7 +293,7 @@ export function MessageBubble({
(mine ? 'right-full pr-2' : 'left-full pl-2')
}
>
<div className="flex items-center gap-0.5 rounded-lg border border-white/10 bg-ink-900/90 p-1 shadow-lg backdrop-blur-xl">
<div className="flex items-center gap-0.5 rounded-lg border border-line bg-surface-3 p-1 shadow-lg">
<ActionButton
label={t('app:friends.action_accept', { defaultValue: 'React' })}
onClick={() => setPickerOpen((v) => !v)}
@@ -306,7 +324,7 @@ export function MessageBubble({
ref={pickerRef}
role="menu"
className={
'absolute z-30 mt-1 flex gap-1 rounded-lg border border-white/10 bg-ink-900/95 p-1.5 shadow-xl backdrop-blur-xl ' +
'absolute z-30 mt-1 flex gap-1 rounded-lg border border-line bg-surface-3 p-1.5 shadow-xl ' +
(mine ? 'right-0' : 'left-0')
}
>
@@ -315,7 +333,7 @@ export function MessageBubble({
key={e}
type="button"
onClick={() => void handlePickEmoji(e)}
className="cursor-pointer rounded-md px-2 py-1 text-lg transition hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400/40"
className="cursor-pointer rounded-md px-2 py-1 text-lg transition hover:bg-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
{e}
</button>
@@ -323,7 +341,7 @@ export function MessageBubble({
<button
type="button"
onClick={() => setPickerOpen(false)}
className="cursor-pointer rounded-md px-1.5 py-1 text-neutral-500 transition hover:bg-white/10"
className="cursor-pointer rounded-md px-1.5 py-1 text-fg-muted transition hover:bg-surface-2"
>
<XIcon className="h-4 w-4" />
</button>
@@ -336,6 +354,27 @@ export function MessageBubble({
);
}
function AvatarSlot({
show,
url,
displayName,
}: {
show: boolean;
url: string | null;
displayName: string | null;
}) {
if (!show) {
return <div aria-hidden="true" className="h-8 w-8 shrink-0" />;
}
return (
<Avatar
url={url}
displayName={displayName ?? ''}
className="h-8 w-8 text-xs"
/>
);
}
function CallEventRow({
parsed,
mine,
@@ -351,8 +390,8 @@ function CallEventRow({
const Icon = isMissed ? PhoneOffIcon : PhoneIcon;
const tone = isMissed
? 'border-rose-500/20 bg-rose-500/10 text-rose-200'
: 'border-emerald-500/20 bg-emerald-500/10 text-emerald-200';
? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-200'
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200';
const label =
status === 'ended'
@@ -408,10 +447,10 @@ function ActionButton({
title={label}
onClick={onClick}
className={
'flex h-7 w-7 cursor-pointer items-center justify-center rounded-md transition focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-400/40 ' +
'flex h-7 w-7 cursor-pointer items-center justify-center rounded-md transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
(tone === 'danger'
? 'text-neutral-400 hover:bg-rose-500/20 hover:text-rose-200'
: 'text-neutral-400 hover:bg-white/10 hover:text-neutral-100')
? 'text-fg-muted hover:bg-rose-500/20 hover:text-rose-500 dark:hover:text-rose-200'
: 'text-fg-muted hover:bg-surface-2 hover:text-fg')
}
>
{icon}