-
-
Notifications
You must be signed in to change notification settings - Fork 22
feat: add typing indicator for better UI experience issue #8 #39
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 1 commit
f47028f
b4b4a8d
b43ac7c
cd1cd9a
c2483dd
87cb3d7
aa771c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,7 @@ import { supabase } from '../lib/supabase'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { detectCrisis, getCrisisResourcesMessage } from './crisisDetection'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface ChatMessage { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'join' | 'leave' | 'chat' | 'crisis_alert'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'join' | 'leave' | 'chat' | 'crisis_alert' | 'typing'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| roomId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nickname?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -79,11 +79,30 @@ export class ChatServer { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'chat': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await this.handleChatMessage(ws, message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'typing': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.handleTyping(ws, message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private handleTyping(ws: WebSocket, message: ChatMessage) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const roomId = (ws as any).roomId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userId = (ws as any).userId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nickname = (ws as any).nickname; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (roomId && userId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Broadcast to other users in the room | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.broadcastToRoom(roomId, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'typing', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nickname, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isTyping: message.content === 'true', // Use content field to convey on/off state if needed, or just presence of event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, userId); // Exclude sender | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Shreenath-14 marked this conversation as resolved.
Outdated
Comment on lines
+103
to
+112
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (roomId && userId) { | |
| const isTyping = typeof message.isTyping === 'boolean' ? message.isTyping : false; | |
| // Broadcast to other users in the room | |
| this.broadcastToRoom(roomId, { | |
| type: 'typing', | |
| userId, | |
| nickname, | |
| isTyping, | |
| }, userId); // Exclude sender | |
| } | |
| // Ensure the socket is still a member of the room before broadcasting | |
| if (!roomId || !userId) { | |
| return; | |
| } | |
| const roomMembers = this.rooms.get(roomId); | |
| if (!roomMembers) { | |
| return; | |
| } | |
| const isMember = Array.from(roomMembers).some((member) => member.ws === ws); | |
| if (!isMember) { | |
| return; | |
| } | |
| const isTyping = typeof message.isTyping === 'boolean' ? message.isTyping : false; | |
| // Broadcast to other users in the room | |
| this.broadcastToRoom( | |
| roomId, | |
| { | |
| type: 'typing', | |
| userId, | |
| nickname, | |
| isTyping, | |
| }, | |
| userId, // Exclude sender | |
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,13 +76,25 @@ export default function ChatRoom({ room, currentUser, onClose }: ChatRoomProps) | |
| type: 'system', | ||
| }, | ||
| ]); | ||
| } else if (message.type === 'crisis_alert') { | ||
| setShowCrisisAlert(true); | ||
| setTimeout(() => setShowCrisisAlert(false), 10000); | ||
|
Shreenath-14 marked this conversation as resolved.
Outdated
|
||
| } else if (message.type === 'typing') { | ||
| const { nickname, isTyping } = message; | ||
|
Shreenath-14 marked this conversation as resolved.
Outdated
|
||
| setTypingUsers((prev) => { | ||
| const newSet = new Set(prev); | ||
| if (isTyping) { | ||
| newSet.add(nickname); | ||
|
Shreenath-14 marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| newSet.delete(nickname); | ||
|
Blazzzeee marked this conversation as resolved.
Outdated
|
||
| } | ||
| return newSet; | ||
| }); | ||
| } | ||
|
Comment on lines
78
to
94
|
||
| }, []); | ||
|
|
||
| const { isConnected, connectionError, sendMessage } = useWebSocket({ | ||
| const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set()); | ||
| const typingTimeoutRef = useRef<NodeJS.Timeout>(); | ||
|
Shreenath-14 marked this conversation as resolved.
Outdated
|
||
|
|
||
| const { isConnected, connectionError, sendMessage, sendTyping } = useWebSocket({ | ||
| roomId: room.id, | ||
| userId: currentUser.id, | ||
| nickname: currentUser.nickname, | ||
|
|
@@ -95,6 +107,23 @@ export default function ChatRoom({ room, currentUser, onClose }: ChatRoomProps) | |
| }, | ||
| }); | ||
|
|
||
| const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| setInputValue(e.target.value); | ||
|
|
||
| // Debounce typing indicator | ||
| if (typingTimeoutRef.current) { | ||
| clearTimeout(typingTimeoutRef.current); | ||
| } else { | ||
| sendTyping(true); | ||
| } | ||
|
|
||
| typingTimeoutRef.current = setTimeout(() => { | ||
| sendTyping(false); | ||
|
Shreenath-14 marked this conversation as resolved.
|
||
| typingTimeoutRef.current = undefined; | ||
| }, 2000); | ||
|
Shreenath-14 marked this conversation as resolved.
|
||
| }; | ||
|
|
||
|
|
||
| useEffect(() => { | ||
| // Scroll to bottom when new messages arrive | ||
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | ||
|
|
@@ -248,13 +277,20 @@ export default function ChatRoom({ room, currentUser, onClose }: ChatRoomProps) | |
| <div ref={messagesEndRef} /> | ||
| </div> | ||
|
|
||
| {/* Typing Indicator */} | ||
| {typingUsers.size > 0 && ( | ||
| <div className="px-4 py-2 text-xs text-gray-500 italic bg-gray-50 border-t border-gray-100"> | ||
| {Array.from(typingUsers).join(', ')} {typingUsers.size === 1 ? 'is' : 'are'} typing... | ||
| </div> | ||
| )} | ||
|
|
||
|
Shreenath-14 marked this conversation as resolved.
|
||
| {/* Input */} | ||
| <form onSubmit={handleSendMessage} className="p-4 border-t border-gray-200 bg-white"> | ||
| <div className="flex gap-2"> | ||
| <input | ||
| type="text" | ||
| value={inputValue} | ||
| onChange={(e) => setInputValue(e.target.value)} | ||
| onChange={handleInputChange} | ||
| placeholder={isConnected ? 'Type your message...' : 'Connecting...'} | ||
| disabled={!isConnected} | ||
| className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 disabled:bg-gray-100 disabled:cursor-not-allowed" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.