import { createSignal, createEffect, For, Show, onMount } from 'solid-js'; import { gql } from '@urql/core'; import { createQuery, createMutation, createSubscription } from '@urql/solid'; import { Message } from '../types'; import 'emoji-picker-element'; import { EmojiClickEvent } from 'emoji-picker-element/shared'; import { Picker } from 'emoji-picker-element'; const ROOM_QUERY = gql` query GetRoom($id: ID!) { room(id: $id) { id name description isPrivate owner { id username } members { id username } messages { id content createdAt user { id username } } } } `; const SEND_MESSAGE_MUTATION = gql` mutation SendMessage($content: String!, $roomId: ID!) { sendMessage(content: $content, roomId: $roomId) { id content createdAt user { id username } } } `; const MESSAGE_SUBSCRIPTION = gql` subscription OnMessageAdded($roomId: ID!) { messageAdded(roomId: $roomId) { id content createdAt user { id username } } } `; const LEAVE_ROOM_MUTATION = gql` mutation LeaveRoom($roomId: ID!) { leaveRoom(roomId: $roomId) } `; interface ChatRoomProps { roomId: string; userId: string; onLeaveRoom?: () => void; } export function ChatRoom(props: ChatRoomProps) { const [variables, setVariables] = createSignal({ id: props.roomId, roomId: props.roomId, }); const [message, setMessage] = createSignal(''); const [messages, setMessages] = createSignal([]); const [confirmLeave, setConfirmLeave] = createSignal(false); const [leaveError, setLeaveError] = createSignal(null); const [showEmojiPicker, setShowEmojiPicker] = createSignal(false); const [emojiPickerRef, setPickerRef] = createSignal( undefined ); let messagesContainer: HTMLDivElement | undefined; let messageInput: HTMLInputElement | undefined; let emojiPickerContainer: HTMLDivElement | undefined; const scrollToBottom = () => { if (messagesContainer) { messagesContainer.scrollTop = messagesContainer.scrollHeight; } }; // Query room details const [roomQuery] = createQuery({ query: ROOM_QUERY, variables: variables, context: { requestPolicy: 'network-only', // Force refetch when variables change }, }); // Send message mutation const [, sendMessage] = createMutation(SEND_MESSAGE_MUTATION); // Subscribe to new messages const [messageSubscription] = createSubscription({ query: MESSAGE_SUBSCRIPTION, variables: variables, pause: false, // Ensure subscription is active }); // Leave room mutation const [leaveRoomResult, leaveRoom] = createMutation(LEAVE_ROOM_MUTATION); // Reset messages when room changes createEffect(() => { // Access props.roomId to create a dependency setVariables({ id: props.roomId, roomId: props.roomId }); // Reset messages when room changes // Clear any errors or confirmations setLeaveError(null); setConfirmLeave(false); }); // Load initial messages createEffect(() => { const result = roomQuery; if (result.data?.room) { setMessages(result.data.room.messages); } }); createEffect(() => { if (messages().length > 0) { scrollToBottom(); } }); // Handle new messages from subscription createEffect(() => { const result = messageSubscription; if (result.data?.messageAdded) { const newMessage = result.data.messageAdded; setMessages((prev) => [...prev, newMessage]); scrollToBottom(); } }); // Handle leave room error createEffect(() => { if (leaveRoomResult.error) { setLeaveError(leaveRoomResult.error.message); setConfirmLeave(false); } }); const MAX_MESSAGE_LENGTH = 2048; const handleSendMessage = async (e: Event) => { e.preventDefault(); const trimmedMessage = message().trim(); if (!trimmedMessage) return; if (trimmedMessage.length > MAX_MESSAGE_LENGTH) return; try { await sendMessage({ content: trimmedMessage, roomId: props.roomId, }); setMessage(''); scrollToBottom(); } catch (error) { console.error('Failed to send message:', error); } }; const handleLeaveRoom = async () => { if (!confirmLeave()) { setConfirmLeave(true); return; } setLeaveError(null); try { const result = await leaveRoom({ roomId: props.roomId }); if (result.data?.leaveRoom) { if (props.onLeaveRoom) { props.onLeaveRoom(); } } } catch (err) { console.error('Failed to leave room:', err); } }; const cancelLeave = () => { setConfirmLeave(false); setLeaveError(null); }; const formatTime = (dateString: string) => { const date = new Date(dateString); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; const toggleEmojiPicker = () => { setShowEmojiPicker(!showEmojiPicker()); }; createEffect(() => { const handleEmojiSelect = (event: EmojiClickEvent) => { setMessage((prev) => prev + event.detail.unicode); messageInput?.focus(); }; const ref = emojiPickerRef(); if (ref) { ref.addEventListener('emoji-click', handleEmojiSelect); return () => { ref.removeEventListener('emoji-click', handleEmojiSelect); }; } }); // Close emoji picker when clicking outside onMount(() => { const handleClickOutside = (event: MouseEvent) => { if ( emojiPickerContainer && !emojiPickerContainer.contains(event.target as Node) ) { setShowEmojiPicker(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }); // Function to generate avatar URL const getUserAvatarUrl = (userId: string, size: number = 40) => `https://avatars.jusemon.com/${userId}?size=${size}`; return (
Loading room...
}>

{roomQuery.data?.room.name}

{roomQuery.data?.room.description}

{leaveError()}
Are you sure you want to leave this room?
Loading messages...
} > {(message) => (
{message.user.id !== props.userId && ( {`${message.user.username}'s )}
{message.user.username} {formatTime(message.createdAt)}
{message.content}
)}
{ const inputValue = e.currentTarget.value; if (inputValue.length <= MAX_MESSAGE_LENGTH) { setMessage(inputValue); } }} placeholder='Type a message...' />
{message().length}/{MAX_MESSAGE_LENGTH}
{showEmojiPicker() && (
{/* @ts-ignore */} setPickerRef(el)}>
)}
); }