feat(federation): add federation status badges to attachment renderer
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { Attachment } from '@backspace/shared';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
import { Tooltip } from '../ui/Tooltip';
|
||||
|
||||
interface AttachmentRendererProps {
|
||||
attachment: Attachment;
|
||||
@@ -28,11 +29,66 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
|
||||
|
||||
const { mimetype, originalName, size } = attachment;
|
||||
|
||||
// Federation status badge
|
||||
const federationBadge = (() => {
|
||||
if (!attachment.federationStatus) return null;
|
||||
|
||||
if (attachment.federationStatus === 'remote') {
|
||||
// Receiving side — file hosted on sender's instance
|
||||
let senderName = 'the sender';
|
||||
if (attachment.federationMeta) {
|
||||
try {
|
||||
const meta = JSON.parse(attachment.federationMeta);
|
||||
if (meta.sourceUsername) senderName = meta.sourceUsername;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return (
|
||||
<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') {
|
||||
// Sending side — file rejected by some peers
|
||||
let tooltipText = 'Some recipients cannot cache this file locally.';
|
||||
if (attachment.federationMeta) {
|
||||
try {
|
||||
const meta: Array<{ username: string; limit: number }> = JSON.parse(attachment.federationMeta);
|
||||
if (meta.length > 0) {
|
||||
const parts = meta.map(u => {
|
||||
const limitMb = Math.round(u.limit / (1024 * 1024));
|
||||
return `${u.username}'s instance limit (${limitMb} MB)`;
|
||||
});
|
||||
tooltipText = `Exceeds ${parts.join(' and ')}. They can still view this file from your instance.`;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return (
|
||||
<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-warning">
|
||||
<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;
|
||||
})();
|
||||
|
||||
if (mimetype.startsWith('image/')) {
|
||||
const { width, height } = attachment;
|
||||
return (
|
||||
<div
|
||||
className="max-w-fit mt-1 rounded-lg overflow-hidden border border-white/[0.06]"
|
||||
className="relative max-w-fit mt-1 rounded-lg overflow-hidden border border-white/[0.06]"
|
||||
style={width && height ? { aspectRatio: `${width}/${height}`, maxWidth: Math.min(width, 400), maxHeight: 300 } : undefined}
|
||||
>
|
||||
<img
|
||||
@@ -42,6 +98,7 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
|
||||
onClick={() => openImagePreview(attUrl)}
|
||||
loading="lazy"
|
||||
/>
|
||||
{federationBadge}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,7 +108,7 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
|
||||
const hasDimensions = width && height;
|
||||
return (
|
||||
<div
|
||||
className="mt-1 max-w-[400px] max-h-[300px] rounded-lg overflow-hidden"
|
||||
className="relative mt-1 max-w-[400px] max-h-[300px] rounded-lg overflow-hidden"
|
||||
style={hasDimensions ? { aspectRatio: `${width}/${height}`, maxHeight: 300 } : undefined}
|
||||
>
|
||||
<video
|
||||
@@ -63,13 +120,14 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
|
||||
<source src={attUrl} type={mimetype} />
|
||||
Your browser does not support video playback.
|
||||
</video>
|
||||
{federationBadge}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mimetype.startsWith('audio/')) {
|
||||
return (
|
||||
<div className="mt-1 flex flex-col p-3 bg-surface-channel/50 rounded-lg border border-border-hard max-w-[420px]">
|
||||
<div className="relative mt-1 flex flex-col p-3 bg-surface-channel/50 rounded-lg border border-border-hard max-w-[420px]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-surface-base rounded text-txt-tertiary flex-shrink-0">
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -90,30 +148,34 @@ export function AttachmentRenderer({ attachment }: AttachmentRendererProps) {
|
||||
<source src={attUrl} type={mimetype} />
|
||||
Your browser does not support audio playback.
|
||||
</audio>
|
||||
{federationBadge}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={attUrl}
|
||||
download={originalName}
|
||||
className="flex items-center gap-3 p-4 bg-surface-channel/50 rounded-lg border border-border-hard hover:bg-interactive-hover transition-all max-w-[400px] mt-1 group/att"
|
||||
>
|
||||
<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">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-txt-link text-[15px] font-medium truncate hover:underline">{originalName}</p>
|
||||
<p className="text-[12px] text-txt-tertiary font-medium">{formatFileSize(size)}</p>
|
||||
</div>
|
||||
</a>
|
||||
<div className="relative inline-block mt-1 max-w-[400px]">
|
||||
<a
|
||||
href={attUrl}
|
||||
download={originalName}
|
||||
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">
|
||||
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-txt-link text-[15px] font-medium truncate hover:underline">{originalName}</p>
|
||||
<p className="text-[12px] text-txt-tertiary font-medium">{formatFileSize(size)}</p>
|
||||
</div>
|
||||
</a>
|
||||
{federationBadge}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user