feat: Improve message attachment handling by checking both fileUrl and content for S3 keys, and add an image loading error fallback UI.
This commit is contained in:
@@ -348,19 +348,26 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
|
||||
|
||||
// Convert S3 keys to presigned download URLs for message attachments
|
||||
useEffect(() => {
|
||||
const isS3Key = (val: string) => val && !val.startsWith('http://') && !val.startsWith('https://') && !val.startsWith('/') && !val.startsWith('data:');
|
||||
const isUrl = (val: string) => val.startsWith('http://') || val.startsWith('https://');
|
||||
|
||||
const convertFileUrls = async () => {
|
||||
for (const msg of messages) {
|
||||
if ((msg.messageType === 'IMAGE' || msg.messageType === 'FILE') && msg.fileUrl) {
|
||||
// Skip if already converted
|
||||
if (messageFileUrls[msg.id]) continue;
|
||||
// Skip if it's already an accessible URL (e.g., Giphy GIF URLs)
|
||||
if (msg.fileUrl.startsWith('http://') || msg.fileUrl.startsWith('https://')) {
|
||||
setMessageFileUrls(prev => ({ ...prev, [msg.id]: msg.fileUrl! }));
|
||||
continue;
|
||||
}
|
||||
// Convert S3 key to presigned download URL
|
||||
if (msg.messageType !== 'IMAGE' && msg.messageType !== 'FILE') continue;
|
||||
if (messageFileUrls[msg.id]) continue;
|
||||
|
||||
// Determine the source: fileUrl or content (some messages store S3 key in content)
|
||||
const source = msg.fileUrl || msg.content;
|
||||
if (!source) continue;
|
||||
|
||||
if (isUrl(source)) {
|
||||
setMessageFileUrls(prev => ({ ...prev, [msg.id]: source }));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isS3Key(source)) {
|
||||
try {
|
||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(msg.fileUrl);
|
||||
const presignedUrl = await uploadService.getPresignedDownloadUrl(source);
|
||||
setMessageFileUrls(prev => ({ ...prev, [msg.id]: presignedUrl }));
|
||||
} catch {
|
||||
// Keep original if conversion fails
|
||||
@@ -886,13 +893,31 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
|
||||
>
|
||||
{message.messageType === 'IMAGE' ? (
|
||||
messageFileUrls[message.id] ? (
|
||||
<img
|
||||
src={messageFileUrls[message.id]}
|
||||
alt={message.fileName || 'Image'}
|
||||
className="rounded-[12px] max-w-full cursor-pointer"
|
||||
style={{ maxHeight: '250px' }}
|
||||
onClick={() => window.open(messageFileUrls[message.id], '_blank')}
|
||||
/>
|
||||
<>
|
||||
<img
|
||||
src={messageFileUrls[message.id]}
|
||||
alt={message.fileName || 'Image'}
|
||||
className="rounded-[12px] max-w-full cursor-pointer"
|
||||
style={{ maxHeight: '250px' }}
|
||||
onClick={() => window.open(messageFileUrls[message.id], '_blank')}
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.style.display = 'none';
|
||||
const fallback = target.nextElementSibling;
|
||||
if (fallback) (fallback as HTMLElement).style.display = 'flex';
|
||||
}}
|
||||
/>
|
||||
<div className="hidden items-center gap-2 px-3 py-2">
|
||||
<Image src="/assets/icons/attachment-icon.svg" alt="Image" width={16} height={9} />
|
||||
<span className="font-serif text-[13px]">{message.fileName || 'Image'}</span>
|
||||
<button
|
||||
onClick={() => window.open(messageFileUrls[message.id], '_blank')}
|
||||
className="font-serif text-[12px] text-[#e58625] underline ml-1"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-[200px] h-[150px] rounded-[12px] overflow-hidden">
|
||||
<div className="w-full h-full shimmer-loading rounded-[12px]" />
|
||||
|
||||
Reference in New Issue
Block a user