From 91cac19a520f2c9d2ce30a6d1ed7e83ebae5bd49 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 5 Feb 2026 19:48:11 +0530 Subject: [PATCH] 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) --- .../components/client/chat/JKChatMessage.js | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/jam-ui/src/components/client/chat/JKChatMessage.js b/jam-ui/src/components/client/chat/JKChatMessage.js index da7479df2..8217ee099 100644 --- a/jam-ui/src/components/client/chat/JKChatMessage.js +++ b/jam-ui/src/components/client/chat/JKChatMessage.js @@ -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 }) => { {senderName || 'Unknown'} {formatTimestamp(createdAt)} -
+
📎 - - attached {attachmentName} - {attachmentSize && ( - ({formatFileSize(attachmentSize)}) - )} + attached + 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} + {attachmentSize && ( + ({formatFileSize(attachmentSize)}) + )}