feat(chat): add country flags and competitor numbers with normalized data architecture
Implemented display of country flags and competitor numbers in event chat messages: - Country flags displayed as emoji (🇸🇪, 🇵🇱, etc.) with proper emoji font support - Competitor numbers shown in #123 format next to usernames - Normalized data architecture with user and participant caches on frontend - User data (username, avatar, country) and participant data (competitorNumber) cached separately - Messages store only core data (id, content, userId, createdAt) - Prevents data inconsistency when users update profile information - Fixed duplicate message keys React warning with deduplication logic - Backend sends nested user/participant objects for cache population - Auto-updates across all messages when user changes avatar or country Backend changes: - Socket.IO event_message and message_history include nested user/participant data - API /events/:slug/messages endpoint restructured with same nested format - Batch lookup of competitor numbers for efficiency Frontend changes: - useEventChat hook maintains userCache and participantCache - ChatMessage component accepts separate user/participant props - ChatMessageList performs cache lookups during render - Emoji font family support for cross-platform flag rendering
This commit is contained in:
@@ -34,6 +34,26 @@ const useEventChat = (slug, userId, event, messagesContainerRef) => {
|
||||
const [loadingOlder, setLoadingOlder] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
// User and participant caches (normalized data)
|
||||
const [userCache, setUserCache] = useState({});
|
||||
const [participantCache, setParticipantCache] = useState({});
|
||||
|
||||
// Helper to update caches from message data
|
||||
const updateCaches = (messageData) => {
|
||||
if (messageData.user) {
|
||||
setUserCache(prev => ({
|
||||
...prev,
|
||||
[messageData.userId]: messageData.user
|
||||
}));
|
||||
}
|
||||
if (messageData.participant) {
|
||||
setParticipantCache(prev => ({
|
||||
...prev,
|
||||
[messageData.userId]: messageData.participant
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Socket.IO connection and event listeners
|
||||
useEffect(() => {
|
||||
if (!event) return;
|
||||
@@ -81,13 +101,44 @@ const useEventChat = (slug, userId, event, messagesContainerRef) => {
|
||||
|
||||
// Receive message history (initial 20 messages)
|
||||
socket.on('message_history', (history) => {
|
||||
setMessages(history);
|
||||
// Update caches from all messages
|
||||
history.forEach(msg => updateCaches(msg));
|
||||
|
||||
// Store only core message data
|
||||
const coreMessages = history.map(msg => ({
|
||||
id: msg.id,
|
||||
userId: msg.userId,
|
||||
content: msg.content,
|
||||
createdAt: msg.createdAt,
|
||||
roomId: msg.roomId,
|
||||
type: msg.type,
|
||||
}));
|
||||
|
||||
setMessages(coreMessages);
|
||||
setHasMore(history.length === 20);
|
||||
});
|
||||
|
||||
// Receive new messages
|
||||
socket.on('event_message', (message) => {
|
||||
setMessages((prev) => [...prev, message]);
|
||||
// Update caches with user/participant data
|
||||
updateCaches(message);
|
||||
|
||||
setMessages((prev) => {
|
||||
// Prevent duplicates - check if message ID already exists
|
||||
if (prev.some(m => m.id === message.id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Store only core message data
|
||||
return [...prev, {
|
||||
id: message.id,
|
||||
userId: message.userId,
|
||||
content: message.content,
|
||||
createdAt: message.createdAt,
|
||||
roomId: message.roomId,
|
||||
type: message.type,
|
||||
}];
|
||||
});
|
||||
});
|
||||
|
||||
// Receive active users list
|
||||
@@ -161,13 +212,29 @@ const useEventChat = (slug, userId, event, messagesContainerRef) => {
|
||||
const response = await eventsAPI.getMessages(slug, oldestMessageId, 20);
|
||||
|
||||
if (response.data.length > 0) {
|
||||
// Update caches from all loaded messages
|
||||
response.data.forEach(msg => updateCaches(msg));
|
||||
|
||||
// Save current scroll position
|
||||
const container = messagesContainerRef.current;
|
||||
const oldScrollHeight = container?.scrollHeight || 0;
|
||||
const oldScrollTop = container?.scrollTop || 0;
|
||||
|
||||
// Prepend older messages
|
||||
setMessages((prev) => [...response.data, ...prev]);
|
||||
// Prepend older messages (filter out any duplicates) - store only core data
|
||||
setMessages((prev) => {
|
||||
const existingIds = new Set(prev.map(m => m.id));
|
||||
const newMessages = response.data
|
||||
.filter(m => !existingIds.has(m.id))
|
||||
.map(msg => ({
|
||||
id: msg.id,
|
||||
userId: msg.userId,
|
||||
content: msg.content,
|
||||
createdAt: msg.createdAt,
|
||||
roomId: msg.roomId,
|
||||
type: msg.type,
|
||||
}));
|
||||
return [...newMessages, ...prev];
|
||||
});
|
||||
setHasMore(response.hasMore);
|
||||
|
||||
// Restore scroll position (adjust for new content)
|
||||
@@ -196,6 +263,9 @@ const useEventChat = (slug, userId, event, messagesContainerRef) => {
|
||||
isConnected,
|
||||
loadingOlder,
|
||||
hasMore,
|
||||
// Caches for normalized data
|
||||
userCache,
|
||||
participantCache,
|
||||
// Actions
|
||||
sendMessage,
|
||||
loadOlderMessages
|
||||
|
||||
Reference in New Issue
Block a user