Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 89 additions & 68 deletions src/components/Chat/LiveChat.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,71 @@
import React, { useState, useEffect, useRef } from 'react';
import { FiSend, FiDollarSign, FiMessageCircle, FiWifi, FiWifiOff, FiLoader, FiRefreshCw } from 'react-icons/fi';
import { toast } from 'react-toastify';
import { useAuth } from '../../context/AuthContext';
import ChatMessage from './ChatMessage';
import SuperChatModal from './SuperChatModal';
import useWebSocket from '../../hooks/useWebSocket';
import api from '../../services/api';
import { useRef, useState } from "react";
import {
FiDollarSign,
FiLoader,
FiMessageCircle,
FiRefreshCw,
FiSend,
FiWifi,
FiWifiOff,
} from "react-icons/fi";
import { toast } from "react-toastify";
import { useAuth } from "../../context/AuthContext";
import { useChatScroll } from "../../hooks/useChatScroll";
import useWebSocket from "../../hooks/useWebSocket";
import ChatMessage from "./ChatMessage";
import SuperChatModal from "./SuperChatModal";

const LiveChat = () => {
const { user } = useAuth();
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [inputMessage, setInputMessage] = useState("");
const [showSuperChatModal, setShowSuperChatModal] = useState(false);
const [superChats, setSuperChats] = useState([]);
const messagesEndRef = useRef(null);

const { sendMessage, isConnected, connectionStatus, manualReconnect } = useWebSocket({
onMessage: (message) => {
console.log('LiveChat received message:', message);
setMessages(prev => {
console.log('Previous messages:', prev.length);
const updated = [...prev, message];
console.log('Updated messages:', updated.length);
return updated;
});
scrollToBottom();
},
onConnect: () => {
console.log('WebSocket connected in LiveChat');
fetchRecentMessages();
},
onDisconnect: () => {
console.log('WebSocket disconnected in LiveChat');
}
});

useEffect(() => {
// fetchSuperChats();
// const interval = setInterval(fetchSuperChats, 30000);
// return () => clearInterval(interval);
}, []);
const chatWindowRef = useChatScroll(messages);

const { sendMessage, isConnected, connectionStatus, manualReconnect } =
useWebSocket({
onMessage: (message) => {
console.log("LiveChat received message:", message);
setMessages((prev) => {
console.log("Previous messages:", prev.length);
const updated = [...prev, message];
console.log("Updated messages:", updated.length);
return updated;
});
// scrollToBottom();
},
onConnect: () => {
console.log("WebSocket connected in LiveChat");
fetchRecentMessages();
},
onDisconnect: () => {
console.log("WebSocket disconnected in LiveChat");
},
});

useEffect(() => {
scrollToBottom();
}, [messages]);
// useEffect(() => {
// // fetchSuperChats();
// // const interval = setInterval(fetchSuperChats, 30000);
// // return () => clearInterval(interval);
// }, []);

// useEffect(() => {
// // scrollToBottom();
// }, [messages]);

const fetchRecentMessages = async () => {
try {
// Recent messages will come through WebSocket connection, not HTTP API
// This function is called onConnect, but recent messages are sent automatically
// by the WebSocket server when client connects, so we don't need to fetch separately
console.log('WebSocket connected - waiting for recent messages...');
console.log("WebSocket connected - waiting for recent messages...");
// Clear messages when reconnecting to avoid duplicates
setMessages([]);
} catch (error) {
console.error('Error in fetchRecentMessages:', error);
console.error("Error in fetchRecentMessages:", error);
}
};

Expand All @@ -69,66 +80,69 @@ const LiveChat = () => {

const handleSendMessage = (e) => {
e.preventDefault();

if (!user) {
toast.warning('Please login to send messages');
toast.warning("Please login to send messages");
return;
}

if (!inputMessage.trim()) return;

if (isConnected) {
sendMessage({
type: 'text',
type: "text",
message: inputMessage,
username: user.email?.split('@')[0] || 'Anonymous',
user_id: user.id
username: user.email?.split("@")[0] || "Anonymous",
user_id: user.id,
});
setInputMessage('');
setInputMessage("");
} else {
toast.error('Chat connection lost. Please refresh the page.');
toast.error("Chat connection lost. Please refresh the page.");
}
};

const handleSuperChatSuccess = (superchat) => {
sendMessage({
type: 'superchat',
type: "superchat",
message: superchat.message,
username: user.email?.split('@')[0] || 'Anonymous',
username: user.email?.split("@")[0] || "Anonymous",
user_id: user.id,
amount: superchat.amount
amount: superchat.amount,
});
setShowSuperChatModal(false);
// fetchSuperChats();
toast.success('SuperChat sent successfully!');
toast.success("SuperChat sent successfully!");
};

const scrollToBottom = () => {
setTimeout(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, 100);
};

return (
<div className="bg-white rounded-lg shadow-lg flex flex-col" style={{ height: '600px' }}>
<div
className="bg-white rounded-lg shadow-lg flex flex-col"
style={{ height: "600px" }}
>
Comment on lines +124 to +127
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix mobile height: avoid fixed 600px to prevent double-scroll and iOS toolbar issues

A fixed pixel height is brittle on mobile and can cause the very scroll bug you’re fixing. Use responsive dvh with a desktop fallback.

-    <div
-      className="bg-white rounded-lg shadow-lg flex flex-col"
-      style={{ height: "600px" }}
-    >
+    <div
+      className="bg-white rounded-lg shadow-lg flex flex-col h-[70dvh] md:h-[600px]"
+    >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className="bg-white rounded-lg shadow-lg flex flex-col"
style={{ height: "600px" }}
>
<div
className="bg-white rounded-lg shadow-lg flex flex-col h-[70dvh] md:h-[600px]"
>
🤖 Prompt for AI Agents
In src/components/Chat/LiveChat.js around lines 124-127, replace the fixed
inline height of "600px" with a responsive dvh-based value and a desktop
fallback to avoid double-scrolling on mobile; update the style to use CSS
min/max (for example height: "min(600px, 100dvh)" or height: "100dvh" with
maxHeight: "600px") so mobile uses 100dvh while desktop is capped, keeping the
element responsive and preventing iOS toolbar scroll issues.

<div className="bg-theme-accent-yellow text-theme-black p-3 rounded-t-lg flex-shrink-0">
<h2 className="text-lg font-bold flex items-center">
<FiMessageCircle className="mr-2" />
Live Chat
<div className="ml-auto flex items-center space-x-2">
{connectionStatus === 'connected' && (
{connectionStatus === "connected" && (
<span className="text-xs bg-green-600 text-white px-2 py-1 rounded flex items-center">
<FiWifi className="mr-1" />
Connected
</span>
)}
{connectionStatus === 'connecting' && (
{connectionStatus === "connecting" && (
<span className="text-xs bg-yellow-600 text-white px-2 py-1 rounded flex items-center">
<FiLoader className="mr-1 animate-spin" />
Connecting...
</span>
)}
{connectionStatus === 'disconnected' && (
{connectionStatus === "disconnected" && (
<>
<span className="text-xs bg-red-600 text-white px-2 py-1 rounded flex items-center">
<FiWifiOff className="mr-1" />
Expand All @@ -150,23 +164,30 @@ const LiveChat = () => {

<div className="bg-blue-50 border-b p-2 flex-shrink-0">
<div className="text-xs text-blue-800">
<span className="font-semibold">ℹ️ Chat Info:</span> All messages are completely anonymous.
Inappropriate words are censored. Mods will remove personal targeted messages.
<span className="font-semibold">ℹ️ Chat Info:</span> All messages are
completely anonymous. Inappropriate words are censored. Mods will
remove personal targeted messages.
Comment on lines +167 to +169
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

User-facing copy contradicts behavior

UI says “completely anonymous” but messages display username (derived from email). This is misleading.

  • Either change copy to reflect display name usage, or
  • Send username: "Anonymous" when isAnonymous is desired.

Example copy:

-<span className="font-semibold">ℹ️ Chat Info:</span> All messages are completely anonymous. Inappropriate words are censored. Mods will remove personal targeted messages.
+<span className="font-semibold">ℹ️ Chat Info:</span> Your display name is shown with messages. Inappropriate words are censored. Mods will remove personal targeted messages.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span className="font-semibold">ℹ️ Chat Info:</span> All messages are
completely anonymous. Inappropriate words are censored. Mods will
remove personal targeted messages.
<span className="font-semibold">ℹ️ Chat Info:</span> Your display name is shown with messages. Inappropriate words are censored. Mods will remove personal targeted messages.
🤖 Prompt for AI Agents
In src/components/Chat/LiveChat.js around lines 167-169, the UI text claims
messages are "completely anonymous" but the app displays a username (derived
from email), which is misleading; either update the copy to accurately state
that a display name/username is shown (e.g., "Messages are pseudonymous; a
display name will be shown") or, if true anonymity is required, ensure when
isAnonymous is set you override the outgoing message payload to set username to
"Anonymous" (and strip any email-derived identifier) before sending; pick one
approach and implement the corresponding change consistently in the message
send/display logic and the copy here.

</div>
</div>

<div className="flex-1 overflow-y-auto p-3 space-y-2 min-h-0">
<div
className="flex-1 overflow-y-auto p-3 space-y-2 min-h-0"
ref={chatWindowRef}
>
{messages.length === 0 ? (
<div className="text-center text-gray-500 py-8">
<FiMessageCircle className="text-3xl mx-auto mb-2 text-gray-300" />
<p className="text-sm">No messages yet. Be the first to chat!</p>
</div>
) : (
messages.map((msg, index) => (
<ChatMessage key={msg.ID || msg.id || `msg-${index}-${Date.now()}`} message={msg} />
<ChatMessage
key={msg.ID || msg.id || `msg-${index}-${Date.now()}`}
message={msg}
/>
Comment on lines +184 to +187
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use stable keys to prevent re-mounts and scroll jumps

Date.now() in keys forces re-mounts every render and can break autoscroll on mobile. Prefer server IDs or timestamps; fall back to index only as last resort.

-            <ChatMessage
-              key={msg.ID || msg.id || `msg-${index}-${Date.now()}`}
-              message={msg}
-            />
+            <ChatMessage
+              key={msg.id || msg.ID || msg.created_at || msg.CreatedAt || index}
+              message={msg}
+            />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ChatMessage
key={msg.ID || msg.id || `msg-${index}-${Date.now()}`}
message={msg}
/>
<ChatMessage
key={msg.id || msg.ID || msg.created_at || msg.CreatedAt || index}
message={msg}
/>
🤖 Prompt for AI Agents
In src/components/Chat/LiveChat.js around lines 184 to 187, the key for
ChatMessage uses Date.now() which forces re-mounts every render and breaks
autoscroll; replace that unstable key with a stable identifier by using the
server-provided ID or timestamp first (e.g., msg.id, msg.ID, msg.timestamp,
msg.createdAt) and only fall back to the array index as last resort (e.g.,
`msg-${index}`) so keys remain stable across renders.

))
)}
<div ref={messagesEndRef} />
{/* <div ref={messagesEndRef} /> */}
</div>

<div className="border-t p-3 flex-shrink-0">
Expand All @@ -177,17 +198,17 @@ const LiveChat = () => {
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
placeholder={
connectionStatus === 'connected'
? "Type your message..."
: connectionStatus === 'connecting'
connectionStatus === "connected"
? "Type your message..."
: connectionStatus === "connecting"
? "Connecting to chat..."
: "Chat disconnected - click retry to reconnect"
}
disabled={connectionStatus !== 'connected'}
disabled={connectionStatus !== "connected"}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-theme-accent-yellow disabled:bg-gray-100 disabled:text-gray-500"
maxLength={200}
/>
<div
<div
className="relative group"
title="SuperChat feature coming soon - PR in progress"
>
Expand Down Expand Up @@ -227,4 +248,4 @@ const LiveChat = () => {
);
};

export default LiveChat;
export default LiveChat;
22 changes: 22 additions & 0 deletions src/hooks/useChatScroll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";

export function useChatScroll(dep) {
const ref = React.useRef(null);

React.useLayoutEffect(() => {
const node = ref.current;
if (node) {
const shouldScroll =
node.scrollTop + node.clientHeight + 20 >= node.scrollHeight;

if (shouldScroll) {
node.scrollTo({
top: node.scrollHeight,
behavior: "smooth",
});
}
}
}, [dep]); // The effect runs whenever the dependency 'dep' changes.

return ref;
}