feat: enhance room management and UI with join/leave functionality

- Added GraphQL mutations and subscriptions for joining and leaving rooms
- Implemented room member tracking and display in room list
- Added leave room confirmation and error handling in chat room
- Updated room list and chat room components with new interaction features
- Improved UI with member badges, join/leave buttons, and error messages
- Enhanced room query to include member information
This commit is contained in:
Juan Sebastián Montoya 2025-03-04 01:46:30 -05:00
parent 16731409df
commit c737258aed
6 changed files with 339 additions and 49 deletions

View file

@ -1,7 +1,7 @@
import { createSignal, createEffect, For, Show } from 'solid-js';
import { gql } from '@urql/core';
import { createQuery, createMutation, createSubscription } from '@urql/solid';
import { Message, Room } from '../types';
import { Message } from '../types';
const ROOM_QUERY = gql`
query GetRoom($id: ID!) {
@ -18,19 +18,14 @@ const ROOM_QUERY = gql`
id
username
}
}
}
`;
const MESSAGES_QUERY = gql`
query GetMessages($roomId: ID!) {
messages(roomId: $roomId) {
id
content
createdAt
user {
messages {
id
username
content
createdAt
user {
id
username
}
}
}
}
@ -64,25 +59,35 @@ const MESSAGE_SUBSCRIPTION = gql`
}
`;
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<Message[]>([]);
const [confirmLeave, setConfirmLeave] = createSignal(false);
const [leaveError, setLeaveError] = createSignal<string | null>(null);
// Query room details
const [roomQuery] = createQuery({
query: ROOM_QUERY,
variables: { id: props.roomId },
});
// Query messages
const [messagesQuery] = createQuery({
query: MESSAGES_QUERY,
variables: { roomId: props.roomId },
variables: variables,
context: {
requestPolicy: 'network-only', // Force refetch when variables change
},
});
// Send message mutation
@ -91,14 +96,28 @@ export function ChatRoom(props: ChatRoomProps) {
// Subscribe to new messages
const [messageSubscription] = createSubscription({
query: MESSAGE_SUBSCRIPTION,
variables: { roomId: props.roomId },
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 = messagesQuery;
if (result.data?.messages) {
setMessages(result.data.messages);
const result = roomQuery;
if (result.data?.room) {
setMessages(result.data.room.messages);
}
});
@ -111,6 +130,14 @@ export function ChatRoom(props: ChatRoomProps) {
}
});
// Handle leave room error
createEffect(() => {
if (leaveRoomResult.error) {
setLeaveError(leaveRoomResult.error.message);
setConfirmLeave(false);
}
});
const handleSendMessage = async (e: Event) => {
e.preventDefault();
@ -127,6 +154,30 @@ export function ChatRoom(props: ChatRoomProps) {
}
};
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' });
@ -138,6 +189,23 @@ export function ChatRoom(props: ChatRoomProps) {
<div class='chat-header'>
<h2>{roomQuery.data?.room.name}</h2>
<p>{roomQuery.data?.room.description}</p>
<div class='room-actions'>
<Show when={leaveError()}>
<div class='error-message'>{leaveError()}</div>
</Show>
<Show when={confirmLeave()}>
<div class='confirm-leave'>
<span>Are you sure you want to leave this room?</span>
<button onClick={handleLeaveRoom}>Yes</button>
<button onClick={cancelLeave}>No</button>
</div>
</Show>
<Show when={!confirmLeave()}>
<button class='leave-button' onClick={handleLeaveRoom}>
Leave Room
</button>
</Show>
</div>
</div>
</Show>

View file

@ -1,6 +1,6 @@
import { createSignal, createEffect, For, Show } from 'solid-js';
import { gql } from '@urql/core';
import { createQuery, createSubscription } from '@urql/solid';
import { createQuery, createSubscription, createMutation } from '@urql/solid';
import { Room } from '../types';
const ROOMS_QUERY = gql`
@ -11,6 +11,9 @@ const ROOMS_QUERY = gql`
description
isPrivate
createdAt
members {
id
}
}
}
`;
@ -23,6 +26,36 @@ const ROOM_ADDED_SUBSCRIPTION = gql`
description
isPrivate
createdAt
members {
id
}
}
}
`;
const ROOM_UPDATED_SUBSCRIPTION = gql`
subscription OnRoomUpdated {
roomUpdated {
id
name
description
isPrivate
createdAt
members {
id
}
}
}
`;
const JOIN_ROOM_MUTATION = gql`
mutation JoinRoom($roomId: ID!) {
joinRoom(roomId: $roomId) {
id
name
members {
id
}
}
}
`;
@ -34,10 +67,15 @@ interface RoomListProps {
export function RoomList(props: RoomListProps) {
const [rooms, setRooms] = createSignal<Room[]>([]);
const [error, setError] = createSignal<string | null>(null);
const [isJoining, setIsJoining] = createSignal(false);
// Query rooms
const [roomsQuery] = createQuery({
query: ROOMS_QUERY,
context: {
requestPolicy: 'network-only',
},
});
// Subscribe to new rooms
@ -45,6 +83,14 @@ export function RoomList(props: RoomListProps) {
query: ROOM_ADDED_SUBSCRIPTION,
});
// Subscribe to room updates (when members change)
const [roomUpdatedSubscription] = createSubscription({
query: ROOM_UPDATED_SUBSCRIPTION,
});
// Join room mutation
const [joinRoomResult, joinRoom] = createMutation(JOIN_ROOM_MUTATION);
// Load initial rooms
createEffect(() => {
const result = roomsQuery;
@ -68,18 +114,93 @@ export function RoomList(props: RoomListProps) {
}
});
// Handle room updates from subscription
createEffect(() => {
const result = roomUpdatedSubscription;
if (result.data?.roomUpdated) {
const updatedRoom = result.data.roomUpdated;
setRooms((prev) => {
return prev.map((room) => {
if (room.id === updatedRoom.id) {
return { ...room, ...updatedRoom };
}
return room;
});
});
}
});
// Handle join room error
createEffect(() => {
if (joinRoomResult.error) {
setError(joinRoomResult.error.message);
setIsJoining(false);
}
});
const handleJoinRoom = async (roomId: string, event: MouseEvent) => {
event.stopPropagation();
setError(null);
setIsJoining(true);
try {
const result = await joinRoom({ roomId });
if (result.data?.joinRoom) {
// Update the local room data with the joined room
setRooms((prev) =>
prev.map((room) =>
room.id === roomId
? { ...room, members: result.data.joinRoom.members }
: room
)
);
// Select the room after joining
props.onSelectRoom(roomId);
}
} catch (err) {
console.error('Failed to join room:', err);
} finally {
setIsJoining(false);
}
};
// Check if the current user is a member of a room
const isMember = (room: Room) => {
const userId = localStorage.getItem('userId');
return room.members?.some((member) => member.id === userId);
};
return (
<div class='room-list'>
<h2>Chat Rooms</h2>
<Show when={error()}>
<div class='error-message'>{error()}</div>
</Show>
<Show when={!roomsQuery.fetching} fallback={<div>Loading rooms...</div>}>
<For each={rooms()}>
{(room) => (
<div
class={`room-item ${props.selectedRoomId === room.id ? 'selected' : ''}`}
onClick={() => props.onSelectRoom(room.id)}
>
<div class='room-name'>{room.name}</div>
<div class='room-description'>{room.description}</div>
<div class='room-item'>
<div
class={`room-info ${props.selectedRoomId === room.id ? 'selected' : ''}`}
onClick={() =>
isMember(room) ? props.onSelectRoom(room.id) : null
}
>
<div class='room-name'>
{room.name}
{isMember(room) && <span class='member-badge'>Member</span>}
</div>
<div class='room-description'>{room.description}</div>
</div>
<Show when={!isMember(room)}>
<button
class='join-button'
onClick={(e) => handleJoinRoom(room.id, e)}
disabled={isJoining()}
>
{isJoining() ? 'Joining...' : 'Join'}
</button>
</Show>
</div>
)}
</For>