-
Notifications
You must be signed in to change notification settings - Fork 7
fix: Fix mobile scroll bug on live chat #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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); | ||||||||||||||||||
| } | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -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" }} | ||||||||||||||||||
| > | ||||||||||||||||||
| <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" /> | ||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User-facing copy contradicts behavior UI says “completely anonymous” but messages display
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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| </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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use stable keys to prevent re-mounts and scroll jumps
- <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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| )) | ||||||||||||||||||
| )} | ||||||||||||||||||
| <div ref={messagesEndRef} /> | ||||||||||||||||||
| {/* <div ref={messagesEndRef} /> */} | ||||||||||||||||||
| </div> | ||||||||||||||||||
|
|
||||||||||||||||||
| <div className="border-t p-3 flex-shrink-0"> | ||||||||||||||||||
|
|
@@ -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" | ||||||||||||||||||
| > | ||||||||||||||||||
|
|
@@ -227,4 +248,4 @@ const LiveChat = () => { | |||||||||||||||||
| ); | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export default LiveChat; | ||||||||||||||||||
| export default LiveChat; | ||||||||||||||||||
| 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; | ||
| } |
There was a problem hiding this comment.
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
dvhwith a desktop fallback.📝 Committable suggestion
🤖 Prompt for AI Agents