feat: Implement rich media messaging for images, files, and GIFs, and add message read/delivery status indicators.
This commit is contained in:
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user