import { useCallback, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { NetworkError } from '../api/client' import { listMyRooms, listRoomMembers } from '../api/rooms' import { BrowseRoomsModal } from '../components/BrowseRoomsModal' import { ChatPane } from '../components/ChatPane' import { NewRoomModal } from '../components/NewRoomModal' import { OfflineBanner } from '../components/OfflineBanner' import { PeopleModal } from '../components/PeopleModal' import { RoomInfoPanel } from '../components/RoomInfoPanel' import { Sidebar } from '../components/Sidebar' import { TopBar } from '../components/TopBar' import { useAuth } from '../context/AuthContext' import { useChatSocketContext } from '../context/ChatSocketContext' import { MOBILE_BREAKPOINT, useWindowWidth } from '../hooks/useWindowWidth' import type { MyRoomItem, RoomMember } from '../types' import './ChatShellPage.css' type ModalKind = 'new' | 'browse' | 'people' | null export function ChatShellPage() { const { roomId } = useParams<{ roomId?: string }>() const navigate = useNavigate() const { user } = useAuth() const width = useWindowWidth() const isMobile = width < MOBILE_BREAKPOINT const [rooms, setRooms] = useState([]) const [search, setSearch] = useState('') const [members, setMembers] = useState([]) const [infoOpen, setInfoOpen] = useState(false) const [modal, setModal] = useState(null) const [roomsUnavailableOffline, setRoomsUnavailableOffline] = useState(false) const activeRoom = rooms.find((r) => r.id === roomId) const refreshRooms = useCallback(async () => { try { const list = await listMyRooms() setRooms(list) setRoomsUnavailableOffline(false) return list } catch (err) { if (err instanceof NetworkError) { setRoomsUnavailableOffline(true) return [] } throw err } }, []) const refreshMembers = useCallback(() => { if (!roomId) return listRoomMembers(roomId).then(setMembers).catch(() => setMembers([])) }, [roomId]) useEffect(() => { refreshRooms().catch(() => {}) }, [refreshRooms]) const socket = useChatSocketContext() const clearRoomIndicators = useCallback((id: string) => { setRooms((prev) => prev.map((r) => (r.id === id ? { ...r, has_unread: false, has_mention: false } : r))) }, []) useEffect( () => socket.subscribe((envelope) => { if (envelope.type === 'room_added') refreshRooms() else if (envelope.type === 'member_updated' && envelope.room_id === roomId) refreshMembers() else if (envelope.type === 'unread_update') { setRooms((prev) => prev.map((r) => r.id === envelope.room_id ? { ...r, has_unread: true, has_mention: r.has_mention || envelope.mentioned } : r, ), ) } else if (envelope.type === 'dm_presence_update') { setRooms((prev) => prev.map((r) => r.dm_partner && r.dm_partner.user_id === envelope.user_id ? { ...r, dm_partner: { ...r.dm_partner, status: envelope.status } } : r, ), ) } }), [socket, refreshRooms, refreshMembers, roomId], ) useEffect(() => { refreshMembers() // Also re-run when the logged-in user's own profile changes (display // name/avatar) -- refreshMembers() itself doesn't change identity when // only roomId is the same, so without this the currently open room's // member list (and anything resolving avatar/name from it, like // MessageList) would keep showing the pre-edit profile until the room // is reopened. }, [refreshMembers, user?.display_name, user?.avatar_filename]) function goToRoom(id: string) { navigate(`/rooms/${id}`) } return (
{(!isMobile || !roomId) && ( setModal('new')} onOpenBrowse={() => setModal('browse')} onOpenPeople={() => setModal('people')} unavailableOffline={roomsUnavailableOffline && rooms.length === 0} /> )} {(!isMobile || roomId) && (activeRoom ? ( navigate('/rooms')} onToggleInfo={() => setInfoOpen((v) => !v)} infoOpen={infoOpen} socket={socket} onRoomRead={clearRoomIndicators} /> ) : ( !isMobile && (

Select a room to start chatting.

) ))} {infoOpen && activeRoom && ( setInfoOpen(false)} onMembersChanged={refreshMembers} onRoomUpdated={() => refreshRooms()} onRoomDeleted={() => { setInfoOpen(false) navigate('/rooms') refreshRooms() }} onLeft={() => { setInfoOpen(false) navigate('/rooms') refreshRooms() }} /> )}
{modal === 'new' && ( setModal(null)} onCreated={(id) => { setModal(null) refreshRooms().then(() => goToRoom(id)) }} /> )} {modal === 'browse' && ( setModal(null)} onJoined={(id) => { setModal(null) refreshRooms().then(() => goToRoom(id)) }} /> )} {modal === 'people' && ( setModal(null)} onOpenRoom={(id) => { setModal(null) refreshRooms().then(() => goToRoom(id)) }} /> )}
) }