Add resizable room panel, searchable user picker, and direct room membership

Room info panel is now user-resizable (fixing a layout clip at narrow
widths), and every user-selection spot (room membership, admin ownership
transfer) uses a new searchable UserPicker instead of raw text input or
prompt(). Member rows fold role + actions into a single inline dropdown
instead of a row of buttons, so the member list stays usable as rooms grow.

Room invites (the accept/decline flow) are replaced by adding a user to a
room directly -- an admin/owner picks someone and they're a member
immediately, with a "you've been added" notification email instead of an
invite email. Drops the now-unused room_invites table.
This commit is contained in:
2026-08-14 20:40:37 -06:00
parent b724f8a33b
commit 91589ee647
32 changed files with 735 additions and 1052 deletions
+1 -22
View File
@@ -1,11 +1,9 @@
import { useCallback, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { NetworkError } from '../api/client'
import { listMyInvites } from '../api/invites'
import { listMyRooms, listRoomMembers } from '../api/rooms'
import { BrowseRoomsModal } from '../components/BrowseRoomsModal'
import { ChatPane } from '../components/ChatPane'
import { InvitesModal } from '../components/InvitesModal'
import { NewRoomModal } from '../components/NewRoomModal'
import { OfflineBanner } from '../components/OfflineBanner'
import { RoomInfoPanel } from '../components/RoomInfoPanel'
@@ -16,7 +14,7 @@ import { MOBILE_BREAKPOINT, useWindowWidth } from '../hooks/useWindowWidth'
import type { MyRoomItem, RoomMember } from '../types'
import './ChatShellPage.css'
type ModalKind = 'new' | 'browse' | 'invites' | null
type ModalKind = 'new' | 'browse' | null
export function ChatShellPage() {
const { roomId } = useParams<{ roomId?: string }>()
@@ -28,7 +26,6 @@ export function ChatShellPage() {
const [rooms, setRooms] = useState<MyRoomItem[]>([])
const [search, setSearch] = useState('')
const [members, setMembers] = useState<RoomMember[]>([])
const [inviteCount, setInviteCount] = useState(0)
const [infoOpen, setInfoOpen] = useState(false)
const [modal, setModal] = useState<ModalKind>(null)
const [roomsUnavailableOffline, setRoomsUnavailableOffline] = useState(false)
@@ -59,12 +56,6 @@ export function ChatShellPage() {
refreshRooms().catch(() => {})
}, [refreshRooms])
useEffect(() => {
listMyInvites()
.then((list) => setInviteCount(list.length))
.catch(() => {})
}, [])
useEffect(() => {
refreshMembers()
// Also re-run when the logged-in user's own profile changes (display
@@ -92,8 +83,6 @@ export function ChatShellPage() {
onSearchChange={setSearch}
onOpenNewRoom={() => setModal('new')}
onOpenBrowse={() => setModal('browse')}
onOpenInvites={() => setModal('invites')}
inviteCount={inviteCount}
unavailableOffline={roomsUnavailableOffline && rooms.length === 0}
/>
)}
@@ -156,16 +145,6 @@ export function ChatShellPage() {
}}
/>
)}
{modal === 'invites' && (
<InvitesModal
onClose={() => setModal(null)}
onInvitesChanged={setInviteCount}
onAccepted={(id) => {
setModal(null)
refreshRooms().then(() => goToRoom(id))
}}
/>
)}
</div>
)
}