fix(media): aplica estado 'não disponível' a imagem, vídeo, documento e sticker
Estende o tratamento de erro 400/404 do redownload para todos os tipos de mídia. Quando mediaUnavailable=true (payload ausente no backend), cada tipo exibe sua mensagem específica e desabilita o clique: - Imagem: 'Imagem não disponível' - Vídeo: 'Vídeo não disponível' - Documento: 'Arquivo não disponível' + cursor-default - Sticker: 'Figurinha não disponível' DocumentBubble recebe nova prop mediaUnavailable para controle interno. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -163,6 +163,7 @@ interface DocumentBubbleProps {
|
||||
TimestampRow: React.ReactNode
|
||||
onRedownload?: () => void
|
||||
isRedownloading?: boolean
|
||||
mediaUnavailable?: boolean
|
||||
}
|
||||
|
||||
const DOC_TYPES: Record<string, { bg: string; label: string }> = {
|
||||
@@ -177,7 +178,7 @@ const DOC_TYPES: Record<string, { bg: string; label: string }> = {
|
||||
csv: { bg: '#276749', label: 'CSV' },
|
||||
}
|
||||
|
||||
const DocumentBubble: React.FC<DocumentBubbleProps> = ({ msg, fullMediaUrl, TimestampRow, onRedownload, isRedownloading }) => {
|
||||
const DocumentBubble: React.FC<DocumentBubbleProps> = ({ msg, fullMediaUrl, TimestampRow, onRedownload, isRedownloading, mediaUnavailable }) => {
|
||||
const [previewOpen, setPreviewOpen] = useState(false)
|
||||
|
||||
// Derive extension from media_type or media_url
|
||||
@@ -203,6 +204,7 @@ const DocumentBubble: React.FC<DocumentBubbleProps> = ({ msg, fullMediaUrl, Time
|
||||
const isPdf = ext === 'pdf'
|
||||
|
||||
const handleClick = () => {
|
||||
if (mediaUnavailable) return
|
||||
if (!fullMediaUrl) {
|
||||
onRedownload?.()
|
||||
return
|
||||
@@ -219,7 +221,7 @@ const DocumentBubble: React.FC<DocumentBubbleProps> = ({ msg, fullMediaUrl, Time
|
||||
<div>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className="flex items-center gap-3 bg-[#f0f2f5] p-3 rounded-xl border border-black/5 cursor-pointer hover:bg-[#e8eaed] transition-all active:scale-[0.98] min-w-[220px] max-w-[280px]"
|
||||
className={`flex items-center gap-3 bg-[#f0f2f5] p-3 rounded-xl border border-black/5 transition-all min-w-[220px] max-w-[280px] ${mediaUnavailable ? 'cursor-default opacity-60' : 'cursor-pointer hover:bg-[#e8eaed] active:scale-[0.98]'}`}
|
||||
>
|
||||
{/* Type icon */}
|
||||
<div
|
||||
@@ -237,7 +239,7 @@ const DocumentBubble: React.FC<DocumentBubbleProps> = ({ msg, fullMediaUrl, Time
|
||||
{filename}
|
||||
</span>
|
||||
<span className="text-[11px] text-[#667781] mt-[2px]">
|
||||
{isRedownloading ? 'Baixando...' : `${ext.toUpperCase()}${sizeStr ? ` · ${sizeStr}` : ''}`}
|
||||
{isRedownloading ? 'Baixando...' : mediaUnavailable ? 'Arquivo não disponível' : `${ext.toUpperCase()}${sizeStr ? ` · ${sizeStr}` : ''}`}
|
||||
</span>
|
||||
</div>
|
||||
<Download className="w-4 h-4 text-[#8696a0] shrink-0" />
|
||||
@@ -618,8 +620,8 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden w-[268px]">
|
||||
<div
|
||||
className="relative group/media w-[268px] h-[200px] bg-black/5 cursor-pointer overflow-hidden"
|
||||
onClick={() => fullMediaUrl ? (onOpenMedia ? onOpenMedia(msg) : window.open(fullMediaUrl, '_blank')) : handleRedownload()}
|
||||
className={`relative group/media w-[268px] h-[200px] bg-black/5 overflow-hidden ${fullMediaUrl || mediaUnavailable ? '' : 'cursor-pointer'}`}
|
||||
onClick={() => fullMediaUrl ? (onOpenMedia ? onOpenMedia(msg) : window.open(fullMediaUrl, '_blank')) : (mediaUnavailable ? undefined : handleRedownload())}
|
||||
>
|
||||
{fullMediaUrl ? (
|
||||
<img
|
||||
@@ -628,10 +630,10 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
className="w-full h-full object-cover transition-transform group-hover/media:scale-[1.03] block"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-2">
|
||||
<div className={`flex flex-col items-center justify-center w-full h-full gap-2 ${mediaUnavailable ? 'opacity-50' : ''}`}>
|
||||
{isRedownloading
|
||||
? <div className="w-8 h-8 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" />
|
||||
: <><ImageIcon className="w-8 h-8 text-slate-400" /><span className="text-[11px] text-slate-400">Toque para baixar</span></>
|
||||
: <><ImageIcon className="w-8 h-8 text-slate-400" /><span className="text-[11px] text-slate-400">{mediaUnavailable ? 'Imagem não disponível' : 'Toque para baixar'}</span></>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
@@ -658,8 +660,8 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
return (
|
||||
<div className="rounded-lg overflow-hidden w-[268px]">
|
||||
<div
|
||||
className="relative w-[268px] h-[200px] bg-black cursor-pointer group/vid overflow-hidden"
|
||||
onClick={() => fullMediaUrl ? (onOpenMedia ? onOpenMedia(msg) : window.open(fullMediaUrl, '_blank')) : handleRedownload()}
|
||||
className={`relative w-[268px] h-[200px] bg-black overflow-hidden group/vid ${fullMediaUrl || mediaUnavailable ? '' : 'cursor-pointer'}`}
|
||||
onClick={() => fullMediaUrl ? (onOpenMedia ? onOpenMedia(msg) : window.open(fullMediaUrl, '_blank')) : (mediaUnavailable ? undefined : handleRedownload())}
|
||||
>
|
||||
{fullMediaUrl ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/20 hover:bg-black/30 transition-colors">
|
||||
@@ -668,10 +670,10 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40 gap-2">
|
||||
<div className={`absolute inset-0 flex flex-col items-center justify-center bg-black/40 gap-2 ${mediaUnavailable ? 'opacity-50' : ''}`}>
|
||||
{isRedownloading
|
||||
? <div className="w-8 h-8 border-2 border-white/60 border-t-transparent rounded-full animate-spin" />
|
||||
: <><Video className="w-8 h-8 text-white/60" /><span className="text-[11px] text-white/60">Toque para baixar</span></>
|
||||
: <><Video className="w-8 h-8 text-white/60" /><span className="text-[11px] text-white/60">{mediaUnavailable ? 'Vídeo não disponível' : 'Toque para baixar'}</span></>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
@@ -717,7 +719,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
}
|
||||
return <AudioBubble msg={msg} fullMediaUrl={fullMediaUrl} isOut={isOut} TimestampRow={TimestampRow} />;
|
||||
case 'document':
|
||||
return <DocumentBubble msg={msg} fullMediaUrl={fullMediaUrl} TimestampRow={TimestampRow} onRedownload={!fullMediaUrl ? handleRedownload : undefined} isRedownloading={isRedownloading} />;
|
||||
return <DocumentBubble msg={msg} fullMediaUrl={fullMediaUrl} TimestampRow={TimestampRow} onRedownload={!fullMediaUrl ? handleRedownload : undefined} isRedownloading={isRedownloading} mediaUnavailable={mediaUnavailable} />;
|
||||
case 'sticker':
|
||||
return (
|
||||
<div>
|
||||
@@ -725,14 +727,14 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
? <img src={fullMediaUrl} alt="Sticker" className="max-w-[180px] max-h-[180px]" />
|
||||
: (
|
||||
<div
|
||||
onClick={handleRedownload}
|
||||
className="flex items-center gap-2 text-[13px] text-[#667781] italic cursor-pointer hover:text-[#111b21] transition-colors"
|
||||
onClick={mediaUnavailable ? undefined : handleRedownload}
|
||||
className={`flex items-center gap-2 text-[13px] text-[#667781] italic transition-colors ${mediaUnavailable ? 'cursor-default opacity-60' : 'cursor-pointer hover:text-[#111b21]'}`}
|
||||
>
|
||||
{isRedownloading
|
||||
? <div className="w-4 h-4 border-2 border-slate-400 border-t-transparent rounded-full animate-spin" />
|
||||
: <Star className="w-4 h-4" />
|
||||
}
|
||||
{isRedownloading ? 'Baixando...' : 'Toque para baixar figurinha'}
|
||||
{isRedownloading ? 'Baixando...' : mediaUnavailable ? 'Figurinha não disponível' : 'Toque para baixar figurinha'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user