feat: Implement rich media messaging for images, files, and GIFs, and add message read/delivery status indicators.

This commit is contained in:
pradeepkumar
2026-02-21 22:11:42 +05:30
parent 3d2171e0bb
commit c5dc139633
12 changed files with 1237 additions and 118 deletions

View File

@@ -35,7 +35,7 @@ interface UseMessagingReturn {
loadConversations: () => Promise<void>;
selectConversation: (conversationId: string) => Promise<void>;
startConversation: (agentProfileId: string) => Promise<Conversation>;
sendMessage: (content: string, messageType?: MessageType) => Promise<void>;
sendMessage: (content: string, messageType?: MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => Promise<void>;
loadMoreMessages: () => Promise<void>;
startTyping: () => void;
stopTyping: () => void;
@@ -179,12 +179,13 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
// Send a message
const sendMessage = useCallback(
async (content: string, messageType: MessageType = 'TEXT' as MessageType) => {
async (content: string, messageType: MessageType = 'TEXT' as MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => {
if (!currentConversationIdRef.current) return;
const dto: CreateMessageDto = {
content,
messageType,
...fileFields,
};
// Stop typing indicator
@@ -226,6 +227,11 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
// Update conversation in list
const conversationId = currentConversationIdRef.current;
const previewText = messageType === ('IMAGE' as MessageType)
? 'Sent an image'
: messageType === ('FILE' as MessageType)
? 'Sent a file'
: content;
setConversations((prev) =>
prev.map((c) =>
c.id === conversationId
@@ -233,7 +239,7 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
...c,
lastMessageAt: new Date().toISOString(),
lastMessageText:
content.length > 255 ? content.substring(0, 252) + '...' : content,
previewText.length > 255 ? previewText.substring(0, 252) + '...' : previewText,
}
: c
)
@@ -360,10 +366,17 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
if (c.id === message.conversationId) {
const isCurrentConversation =
c.id === currentConversationIdRef.current;
// Show friendly preview for non-text messages
let previewText = message.content.substring(0, 255);
if (message.messageType === 'IMAGE') {
previewText = 'Sent an image';
} else if (message.messageType === 'FILE') {
previewText = 'Sent a file';
}
return {
...c,
lastMessageAt: message.createdAt,
lastMessageText: message.content.substring(0, 255),
lastMessageText: previewText,
unreadCount: isCurrentConversation ? 0 : c.unreadCount + 1,
};
}
@@ -413,38 +426,8 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
}
);
// Handle auth errors (token expired, server disconnected us)
const unsubscribeAuthError = socketService.onAuthError(async () => {
console.log('Socket auth error detected, refreshing token via REST...');
try {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
console.error('No refresh token available for socket reconnection');
return;
}
// Refresh token using the same endpoint as the REST API interceptor
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1'}/auth/refresh`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
}
);
const data = await res.json();
if (res.ok && data.success) {
const newToken = data.data.accessToken;
localStorage.setItem('accessToken', newToken);
localStorage.setItem('refreshToken', data.data.refreshToken);
console.log('Token refreshed, reconnecting socket...');
socketService.reconnectWithToken(newToken);
} else {
console.error('Token refresh failed:', data.message);
}
} catch (error) {
console.error('Failed to refresh token for socket reconnection:', error);
}
});
// Auth error handling (token refresh + reconnect) is handled globally
// by PresenceProvider — no duplicate handler needed here.
return () => {
unsubscribeConnection();
@@ -453,7 +436,6 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet
unsubscribeTypingStop();
unsubscribeStatus();
unsubscribeRead();
unsubscribeAuthError();
};
}, [updateUserStatus]);