fix(federation): fix badge positioning and tooltip text for file attachments

- Use inline badges for file/audio attachments (next to file size)
  instead of absolute positioning which overflowed the container
- Keep overlay badges for images/video (overflow-hidden containers)
- Match tooltip text to toast notification text for consistency
This commit is contained in:
Jannis Braun
2026-03-27 05:07:55 +01:00
parent ceb0c9812d
commit dd3ddd9cc2
@@ -29,12 +29,11 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
const { mimetype, originalName, size } = attachment; const { mimetype, originalName, size } = attachment;
// Federation status badge // Federation status — build tooltip text
const federationBadge = (() => { const federationTooltip = (() => {
if (!attachment.federationStatus) return null; if (!attachment.federationStatus) return null;
if (attachment.federationStatus === 'remote') { if (attachment.federationStatus === 'remote') {
// Receiving side — file hosted on sender's instance
let senderName = 'the sender'; let senderName = 'the sender';
if (attachment.federationMeta) { if (attachment.federationMeta) {
try { try {
@@ -42,48 +41,57 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
if (meta.sourceUsername) senderName = meta.sourceUsername; if (meta.sourceUsername) senderName = meta.sourceUsername;
} catch { /* ignore */ } } catch { /* ignore */ }
} }
return ( return { text: `Hosted on ${senderName}'s instance. Download to keep a local copy.`, type: 'remote' as const };
<Tooltip content={`Hosted on ${senderName}'s instance. Download to keep a local copy.`} position="top">
<div className="absolute top-2 right-2 flex items-center gap-1 px-1.5 py-0.5 rounded glass-pill text-xs text-txt-muted">
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
</svg>
</div>
</Tooltip>
);
} }
if (attachment.federationStatus === 'remote_partial') { if (attachment.federationStatus === 'remote_partial') {
// Sending side — file rejected by some peers let text = 'Some recipients cannot cache this file locally.';
let tooltipText = 'Some recipients cannot cache this file locally.';
if (attachment.federationMeta) { if (attachment.federationMeta) {
try { try {
const meta: Array<{ username: string; limit: number }> = JSON.parse(attachment.federationMeta); const meta: Array<{ username: string; limit: number }> = JSON.parse(attachment.federationMeta);
if (meta.length > 0) { if (meta.length > 0) {
const parts = meta.map(u => { const parts = meta.map(u => {
const limitMb = Math.round(u.limit / (1024 * 1024)); const limitMb = Math.round(u.limit / (1024 * 1024));
return `${u.username}'s instance limit (${limitMb} MB)`; return `${u.username}'s instance (limit: ${limitMb} MB)`;
}); });
tooltipText = `Exceeds ${parts.join(' and ')}. They can still view this file from your instance.`; text = `File couldn't be cached on ${parts.join(' and ')}. They can still view it from yours.`;
} }
} catch { /* ignore */ } } catch { /* ignore */ }
} }
return ( return { text, type: 'remote_partial' as const };
<Tooltip content={tooltipText} position="top">
<div className="absolute top-2 right-2 flex items-center gap-1 px-1.5 py-0.5 rounded glass-pill text-xs text-accent-amber">
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</div>
</Tooltip>
);
} }
return null; return null;
})(); })();
// Overlay badge for media (images/video) — absolute positioned inside overflow-hidden container
const federationOverlayBadge = federationTooltip ? (
<Tooltip content={federationTooltip.text} position="top">
<div className={`absolute top-2 right-2 flex items-center gap-1 px-1.5 py-0.5 rounded glass-pill text-xs ${federationTooltip.type === 'remote' ? 'text-txt-muted' : 'text-accent-amber'}`}>
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{federationTooltip.type === 'remote'
? <path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
: <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /><line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /></>
}
</svg>
</div>
</Tooltip>
) : null;
// Inline badge for file cards (audio/generic) — sits inside the card layout, not absolute
const federationInlineBadge = federationTooltip ? (
<Tooltip content={federationTooltip.text} position="top">
<div className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded glass-pill text-xs cursor-default ${federationTooltip.type === 'remote' ? 'text-txt-muted' : 'text-accent-amber'}`}>
<svg className="w-3 h-3 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{federationTooltip.type === 'remote'
? <path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
: <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" /><line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" /></>
}
</svg>
</div>
</Tooltip>
) : null;
if (mimetype.startsWith('image/')) { if (mimetype.startsWith('image/')) {
const { width, height } = attachment; const { width, height } = attachment;
return ( return (
@@ -98,7 +106,7 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
onClick={() => openImagePreview(attUrl)} onClick={() => openImagePreview(attUrl)}
loading="lazy" loading="lazy"
/> />
{federationBadge} {federationOverlayBadge}
</div> </div>
); );
} }
@@ -120,7 +128,7 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
<source src={attUrl} type={mimetype} /> <source src={attUrl} type={mimetype} />
Your browser does not support video playback. Your browser does not support video playback.
</video> </video>
{federationBadge} {federationOverlayBadge}
</div> </div>
); );
} }
@@ -141,41 +149,43 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-txt-primary text-[14px] font-medium truncate">{originalName}</p> <p className="text-txt-primary text-[14px] font-medium truncate">{originalName}</p>
<p className="text-[12px] text-txt-tertiary">{formatFileSize(size)}</p> <div className="flex items-center gap-2">
<p className="text-[12px] text-txt-tertiary">{formatFileSize(size)}</p>
{federationInlineBadge}
</div>
</div> </div>
</div> </div>
<audio controls preload="metadata" className="w-full mt-2 h-8"> <audio controls preload="metadata" className="w-full mt-2 h-8">
<source src={attUrl} type={mimetype} /> <source src={attUrl} type={mimetype} />
Your browser does not support audio playback. Your browser does not support audio playback.
</audio> </audio>
{federationBadge}
</div> </div>
); );
} }
return ( return (
<div className="relative inline-block mt-1 max-w-[400px]"> <a
<a href={attUrl}
href={attUrl} download={originalName}
download={originalName} className="mt-1 max-w-[400px] flex items-center gap-3 p-4 bg-surface-channel/50 rounded-lg border border-border-hard hover:bg-interactive-hover transition-all group/att"
className="flex items-center gap-3 p-4 bg-surface-channel/50 rounded-lg border border-border-hard hover:bg-interactive-hover transition-all group/att" >
> <div className="p-2 bg-surface-base rounded text-txt-tertiary group-hover/att:text-txt-primary transition-colors">
<div className="p-2 bg-surface-base rounded text-txt-tertiary group-hover/att:text-txt-primary transition-colors"> <svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path
<path strokeLinecap="round"
strokeLinecap="round" strokeLinejoin="round"
strokeLinejoin="round" strokeWidth={1.5}
strokeWidth={1.5} d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
/> </svg>
</svg> </div>
</div> <div className="min-w-0">
<div className="min-w-0"> <p className="text-txt-link text-[15px] font-medium truncate hover:underline">{originalName}</p>
<p className="text-txt-link text-[15px] font-medium truncate hover:underline">{originalName}</p> <div className="flex items-center gap-2">
<p className="text-[12px] text-txt-tertiary font-medium">{formatFileSize(size)}</p> <p className="text-[12px] text-txt-tertiary font-medium">{formatFileSize(size)}</p>
{federationInlineBadge}
</div> </div>
</a> </div>
{federationBadge} </a>
</div>
); );
} }