Prevent contact status bar jitter. Closes #68.

This commit is contained in:
Jack Kingsman
2026-03-17 16:28:05 -07:00
parent bc16d804e9
commit d5b8f7d462
+55 -108
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import { Bell, Globe2, Info, Route, Star, Trash2 } from 'lucide-react'; import { Bell, Globe2, Info, Route, Star, Trash2 } from 'lucide-react';
import { toast } from './ui/sonner'; import { toast } from './ui/sonner';
import { DirectTraceIcon } from './DirectTraceIcon'; import { DirectTraceIcon } from './DirectTraceIcon';
@@ -60,10 +60,8 @@ export function ChatHeader({
onOpenChannelInfo, onOpenChannelInfo,
}: ChatHeaderProps) { }: ChatHeaderProps) {
const [showKey, setShowKey] = useState(false); const [showKey, setShowKey] = useState(false);
const [contactStatusInline, setContactStatusInline] = useState(true);
const [pathDiscoveryOpen, setPathDiscoveryOpen] = useState(false); const [pathDiscoveryOpen, setPathDiscoveryOpen] = useState(false);
const [channelOverrideOpen, setChannelOverrideOpen] = useState(false); const [channelOverrideOpen, setChannelOverrideOpen] = useState(false);
const keyTextRef = useRef<HTMLSpanElement | null>(null);
useEffect(() => { useEffect(() => {
setShowKey(false); setShowKey(false);
@@ -117,43 +115,6 @@ export function ChatHeader({
} }
}; };
useEffect(() => {
if (conversation.type !== 'contact') {
setContactStatusInline(true);
return;
}
const measure = () => {
const keyElement = keyTextRef.current;
if (!keyElement) return;
const isTruncated = keyElement.scrollWidth > keyElement.clientWidth + 1;
setContactStatusInline(!isTruncated);
};
measure();
const onResize = () => {
window.requestAnimationFrame(measure);
};
window.addEventListener('resize', onResize);
let observer: ResizeObserver | null = null;
if (typeof ResizeObserver !== 'undefined') {
observer = new ResizeObserver(() => {
window.requestAnimationFrame(measure);
});
if (keyTextRef.current?.parentElement) {
observer.observe(keyTextRef.current.parentElement);
}
}
return () => {
window.removeEventListener('resize', onResize);
observer?.disconnect();
};
}, [conversation.id, conversation.type, showKey]);
return ( return (
<header className="conversation-header flex justify-between items-start px-4 py-2.5 border-b border-border gap-2"> <header className="conversation-header flex justify-between items-start px-4 py-2.5 border-b border-border gap-2">
<span className="flex min-w-0 flex-1 items-start gap-2"> <span className="flex min-w-0 flex-1 items-start gap-2">
@@ -174,31 +135,16 @@ export function ChatHeader({
/> />
</button> </button>
)} )}
<span className="flex min-w-0 flex-1 flex-col"> <span className="grid min-w-0 flex-1 grid-cols-1 gap-y-0.5 min-[1200px]:grid-cols-[minmax(0,1fr)_auto] min-[1200px]:items-baseline min-[1200px]:gap-x-2">
<span className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5"> <span className="flex min-w-0 items-baseline gap-2 whitespace-nowrap">
<span className="flex min-w-0 flex-1 items-baseline gap-2 whitespace-nowrap"> <h2 className="min-w-0 shrink font-semibold text-base">
<h2 className="min-w-0 shrink font-semibold text-base"> {titleClickable ? (
{titleClickable ? ( <button
<button type="button"
type="button" className="flex min-w-0 shrink items-center gap-1.5 text-left hover:text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
className="flex min-w-0 shrink items-center gap-1.5 text-left hover:text-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm" aria-label={`View info for ${conversation.name}`}
aria-label={`View info for ${conversation.name}`} onClick={handleOpenConversationInfo}
onClick={handleOpenConversationInfo} >
>
<span className="truncate">
{conversation.type === 'channel' &&
!conversation.name.startsWith('#') &&
activeChannel?.is_hashtag
? '#'
: ''}
{conversation.name}
</span>
<Info
className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80"
aria-hidden="true"
/>
</button>
) : (
<span className="truncate"> <span className="truncate">
{conversation.type === 'channel' && {conversation.type === 'channel' &&
!conversation.name.startsWith('#') && !conversation.name.startsWith('#') &&
@@ -207,56 +153,57 @@ export function ChatHeader({
: ''} : ''}
{conversation.name} {conversation.name}
</span> </span>
)} <Info
</h2> className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/80"
{isPrivateChannel && !showKey ? ( aria-hidden="true"
<button />
className="min-w-0 flex-shrink text-[11px] font-mono text-muted-foreground transition-colors hover:text-primary"
onClick={(e) => {
e.stopPropagation();
setShowKey(true);
}}
title="Reveal channel key"
>
Show Key
</button> </button>
) : ( ) : (
<span <span className="truncate">
ref={keyTextRef} {conversation.type === 'channel' &&
className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors hover:text-primary" !conversation.name.startsWith('#') &&
role="button" activeChannel?.is_hashtag
tabIndex={0} ? '#'
onKeyDown={handleKeyboardActivate} : ''}
onClick={(e) => { {conversation.name}
e.stopPropagation();
navigator.clipboard.writeText(conversation.id);
toast.success(
conversation.type === 'channel' ? 'Room key copied!' : 'Contact key copied!'
);
}}
title="Click to copy"
aria-label={
conversation.type === 'channel' ? 'Copy channel key' : 'Copy contact key'
}
>
{conversation.type === 'channel'
? conversation.id.toLowerCase()
: conversation.id}
</span> </span>
)} )}
</span> </h2>
{conversation.type === 'contact' && activeContact && contactStatusInline && ( {isPrivateChannel && !showKey ? (
<span className="min-w-0 flex-none text-[11px] text-muted-foreground"> <button
<ContactStatusInfo className="min-w-0 flex-shrink text-[11px] font-mono text-muted-foreground transition-colors hover:text-primary"
contact={activeContact} onClick={(e) => {
ourLat={config?.lat ?? null} e.stopPropagation();
ourLon={config?.lon ?? null} setShowKey(true);
/> }}
title="Reveal channel key"
>
Show Key
</button>
) : (
<span
className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors hover:text-primary"
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"
aria-label={
conversation.type === 'channel' ? 'Copy channel key' : 'Copy contact key'
}
>
{conversation.type === 'channel' ? conversation.id.toLowerCase() : conversation.id}
</span> </span>
)} )}
</span> </span>
{conversation.type === 'contact' && activeContact && !contactStatusInline && ( {conversation.type === 'contact' && activeContact && (
<span className="mt-0.5 min-w-0 text-[11px] text-muted-foreground"> <span className="min-w-0 text-[11px] text-muted-foreground min-[1200px]:justify-self-end">
<ContactStatusInfo <ContactStatusInfo
contact={activeContact} contact={activeContact}
ourLat={config?.lat ?? null} ourLat={config?.lat ?? null}