Hosted rooms phase 2: moderators, bans, privacy, invites, images, favorites, ratings, recent rooms, occupancy watch
CI / test (push) Successful in 1m10s

Moderators (up to 20 IMVU accounts, set by ID or name) and the owner account boot
and ban over realtime (room.boot); bans are listed and lifted over REST. Private
rooms are unlisted and joined by the owner, moderators and holders of an accepted
invite (room.invite / room.invited / room.invite_reply / room.invite_answer).
Room images (PNG/JPEG by magic bytes, up to 512 KB) are stored in the database
and served with cache headers. Per-person favorites, ratings and recent rooms.
room.watch pushes room.occupancy; participants carry their IMVU name, and
GET .../participants lists them. Contract in docs/hosted-rooms.md, "Phase 2".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 06:07:41 -07:00
1 parent 43183ec7ee
commit 76c8e0342e
16 files changed
+3851 -73

No files matched your search

+12 -4
View File
@@ -4,6 +4,7 @@ import { PageHeader, Tag } from "@/components/status";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { api, getMe, unwrap, type HostedRoom } from "@/lib/api";
import { RoomActions } from "./room-actions";
export const metadata: Metadata = { title: "Rooms" };
@@ -39,7 +40,7 @@ export default async function Rooms() {
<ul className="grid gap-4 sm:grid-cols-2">
{hosted.map((r) => (
<li key={r.n}>
<HostedCard room={r} mine={own.has(r.n)} />
<HostedCard room={r} mine={own.has(r.n)} signedIn={!!me} />
</li>
))}
</ul>
@@ -93,13 +94,18 @@ export default async function Rooms() {
);
}
function HostedCard({ room: r, mine }: { room: HostedRoom; mine: boolean }) {
function HostedCard({ room: r, mine, signedIn }: { room: HostedRoom; mine: boolean; signedIn: boolean }) {
return (
<Card className="h-full">
<CardContent className="space-y-3">
<Card className="h-full overflow-hidden py-0">
{r.image && (
// The room's image, from the hub.
<img src={r.image} alt="" className="aspect-[332/281] w-full bg-muted object-cover" loading="lazy" />
)}
<CardContent className="space-y-3 py-6">
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{r.name}</span>
{r.rating === "adult" && <Tag tone="adult">Adult</Tag>}
{r.privacy === "private" && <Tag>Private</Tag>}
{r.occupancy > 0 && (
<Tag tone="online" dot>
{r.occupancy} here
@@ -113,8 +119,10 @@ function HostedCard({ room: r, mine }: { room: HostedRoom; mine: boolean }) {
</div>
<p className="text-xs text-muted-foreground">
{r.ownerName} · {r.occupancy}/{r.maxUsers}
{r.ratings > 0 && ` · ★ ${r.score.toFixed(1)} (${r.ratings})`}
</p>
{r.description && <p className="line-clamp-3 text-sm whitespace-pre-line">{r.description}</p>}
{signedIn && <RoomActions n={r.n} favorite={r.favorite} canRate={!mine} />}
</CardContent>
</Card>
);