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