Files
Remote-Terminal-for-MeshCore/frontend/src/components/ChatHeader.tsx
T
2026-03-02 20:34:06 -08:00

161 lines
6.1 KiB
TypeScript

import { toast } from './ui/sonner';
import { isFavorite } from '../utils/favorites';
import { handleKeyboardActivate } from '../utils/a11y';
import { ContactAvatar } from './ContactAvatar';
import { ContactStatusInfo } from './ContactStatusInfo';
import type { Contact, Conversation, Favorite, RadioConfig } from '../types';
interface ChatHeaderProps {
conversation: Conversation;
contacts: Contact[];
config: RadioConfig | null;
favorites: Favorite[];
onTrace: () => void;
onToggleFavorite: (type: 'channel' | 'contact', id: string) => void;
onDeleteChannel: (key: string) => void;
onDeleteContact: (publicKey: string) => void;
onOpenContactInfo?: (publicKey: string) => void;
}
export function ChatHeader({
conversation,
contacts,
config,
favorites,
onTrace,
onToggleFavorite,
onDeleteChannel,
onDeleteContact,
onOpenContactInfo,
}: ChatHeaderProps) {
return (
<header className="flex justify-between items-center px-4 py-2.5 border-b border-border gap-2">
<span className="flex flex-wrap items-center gap-x-2 min-w-0 flex-1">
{conversation.type === 'contact' && onOpenContactInfo && (
<span
className="flex-shrink-0 cursor-pointer"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
onClick={() => onOpenContactInfo(conversation.id)}
title="View contact info"
>
<ContactAvatar
name={conversation.name}
publicKey={conversation.id}
size={28}
contactType={contacts.find((c) => c.public_key === conversation.id)?.type}
clickable
/>
</span>
)}
<h2
className={`flex-shrink-0 font-semibold text-base ${conversation.type === 'contact' && onOpenContactInfo ? 'cursor-pointer hover:text-primary transition-colors' : ''}`}
role={conversation.type === 'contact' && onOpenContactInfo ? 'button' : undefined}
tabIndex={conversation.type === 'contact' && onOpenContactInfo ? 0 : undefined}
onKeyDown={
conversation.type === 'contact' && onOpenContactInfo
? handleKeyboardActivate
: undefined
}
onClick={
conversation.type === 'contact' && onOpenContactInfo
? () => onOpenContactInfo(conversation.id)
: undefined
}
>
{conversation.type === 'channel' &&
!conversation.name.startsWith('#') &&
conversation.name !== 'Public'
? '#'
: ''}
{conversation.name}
</h2>
<span
className="font-normal text-[11px] text-muted-foreground font-mono truncate cursor-pointer hover:text-primary transition-colors"
role="button"
tabIndex={0}
onKeyDown={handleKeyboardActivate}
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(conversation.id);
toast.success(
conversation.type === 'channel' ? 'Room key copied!' : 'Contact key copied!'
);
}}
title="Click to copy"
>
{conversation.type === 'channel' ? conversation.id.toLowerCase() : conversation.id}
</span>
{conversation.type === 'contact' &&
(() => {
const contact = contacts.find((c) => c.public_key === conversation.id);
if (!contact) return null;
return (
<ContactStatusInfo
contact={contact}
ourLat={config?.lat ?? null}
ourLon={config?.lon ?? null}
/>
);
})()}
</span>
<div className="flex items-center gap-0.5 flex-shrink-0">
{/* Direct trace button (contacts only) */}
{conversation.type === 'contact' && (
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={onTrace}
title="Direct Trace"
aria-label="Direct Trace"
>
<span aria-hidden="true">&#x1F6CE;</span>
</button>
)}
{/* Favorite button */}
{(conversation.type === 'channel' || conversation.type === 'contact') && (
<button
className="p-1.5 rounded hover:bg-accent text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() =>
onToggleFavorite(conversation.type as 'channel' | 'contact', conversation.id)
}
title={
isFavorite(favorites, conversation.type as 'channel' | 'contact', conversation.id)
? 'Remove from favorites'
: 'Add to favorites'
}
aria-label={
isFavorite(favorites, conversation.type as 'channel' | 'contact', conversation.id)
? 'Remove from favorites'
: 'Add to favorites'
}
>
{isFavorite(favorites, conversation.type as 'channel' | 'contact', conversation.id) ? (
<span className="text-amber-400">&#9733;</span>
) : (
<span className="text-muted-foreground">&#9734;</span>
)}
</button>
)}
{/* Delete button */}
{!(conversation.type === 'channel' && conversation.name === 'Public') && (
<button
className="p-1.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive text-lg leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={() => {
if (conversation.type === 'channel') {
onDeleteChannel(conversation.id);
} else {
onDeleteContact(conversation.id);
}
}}
title="Delete"
aria-label="Delete"
>
<span aria-hidden="true">&#128465;</span>
</button>
)}
</div>
</header>
);
}