feat(14-02): add clickable attachment links with signed URL fetching

- Import getMusicNotationUrl REST helper and useState/useCallback hooks
- Add handleAttachmentClick handler that fetches signed S3 URL and opens in new tab
- Implement loading state (isLoadingUrl) to prevent rapid clicks
- Make filename a clickable link with underline and blue color
- Add error handling that logs to console without crashing
- Show 'wait' cursor during URL fetch
- Browser handles file display based on Content-Type (PDF viewer, image display, audio player)
This commit is contained in:
Nuwan 2026-02-05 19:48:11 +05:30
parent 38fea32f22
commit 91cac19a52
1 changed files with 62 additions and 8 deletions

View File

@ -1,7 +1,8 @@
import React from 'react';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { formatTimestamp } from '../../../utils/formatTimestamp';
import { formatFileSize } from '../../../services/attachmentValidation';
import { getMusicNotationUrl } from '../../../helpers/rest';
/**
* Get initials for avatar from sender name
@ -40,7 +41,35 @@ const isAttachmentMessage = message => {
* - React.memo for performance optimization
*/
const JKChatMessage = ({ message }) => {
const { senderName, message: text, createdAt, attachmentName, attachmentSize } = message;
const { senderName, message: text, createdAt, attachmentId, attachmentName, attachmentSize } = message;
const [isLoadingUrl, setIsLoadingUrl] = useState(false);
/**
* Handle attachment click - fetch signed S3 URL and open in new tab
* @param {string} attachmentId - ID of the attachment to open
*/
const handleAttachmentClick = useCallback(
async attachmentId => {
if (isLoadingUrl) return; // Prevent rapid clicks
try {
setIsLoadingUrl(true);
const response = await getMusicNotationUrl(attachmentId);
// Response is { url: "https://s3.amazonaws.com/...?signature=..." }
if (response && response.url) {
window.open(response.url, '_blank');
} else {
console.error('No URL in response:', response);
}
} catch (error) {
console.error('Failed to fetch attachment URL:', error);
// Could show toast error here, but keep it simple for now
} finally {
setIsLoadingUrl(false);
}
},
[isLoadingUrl]
);
// Common avatar style
const avatarStyle = {
@ -85,16 +114,41 @@ const JKChatMessage = ({ message }) => {
<span style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>{senderName || 'Unknown'}</span>
<span style={{ fontSize: '12px', color: '#6c757d' }}>{formatTimestamp(createdAt)}</span>
</div>
<div style={{ fontSize: '14px', color: '#212529', display: 'flex', alignItems: 'center' }}>
<div
style={{
fontSize: '14px',
color: '#212529',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: '4px'
}}
>
<span style={fileIconStyle} role="img" aria-label="attachment">
📎
</span>
<span>
attached <strong>{attachmentName}</strong>
{attachmentSize && (
<span style={{ color: '#6c757d', marginLeft: '4px' }}>({formatFileSize(attachmentSize)})</span>
)}
<span>attached</span>
<span
onClick={() => handleAttachmentClick(attachmentId)}
style={{
cursor: isLoadingUrl ? 'wait' : 'pointer',
color: '#1976d2',
textDecoration: 'underline',
fontWeight: 600,
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
verticalAlign: 'bottom'
}}
title={attachmentName}
>
{attachmentName}
</span>
{attachmentSize && (
<span style={{ color: '#6c757d', flexShrink: 0 }}>({formatFileSize(attachmentSize)})</span>
)}
</div>
</div>
</div>