///
import {
Cell,
computed,
Default,
handler,
ifElse,
NAME,
navigateTo,
pattern,
UI,
} from "commontools";
import GroupChatRoom, { Message, User } from "./group-chat-room.tsx";
/**
* Group Chat Lobby Pattern
*
* Apple iOS-style lobby where unlimited users can join.
* Shows all joined users and a form for new users.
*/
interface LobbyInput {
chatName: Default;
messages: Cell>;
users: Cell>;
sessionId: Cell>;
}
interface LobbyOutput {
chatName: Default;
messages: Cell>;
users: Cell>;
sessionId: Cell>;
}
// Random color selection from a pool of distinct colors
function getRandomColor(): string {
const colors = [
"#007AFF", // Apple blue
"#34C759", // Apple green
"#FF9500", // Apple orange
"#AF52DE", // Apple purple
"#FF3B30", // Apple red
"#5856D6", // Apple indigo
"#FF2D55", // Apple pink
"#00C7BE", // Apple teal
];
return colors[Math.floor(Math.random() * colors.length)];
}
// Get initials from name
function getInitials(name: string): string {
if (!name || typeof name !== "string") return "?";
return name
.trim()
.split(/\s+/)
.map((word) => word[0])
.join("")
.toUpperCase()
.slice(0, 2);
}
// Handler to reset the lobby (clear all state and generate new session)
const resetLobby = handler<
unknown,
{ messages: Cell; users: Cell; sessionId: Cell }
>((_event, { messages, users, sessionId }) => {
console.log("[resetLobby] Resetting all chat state...");
// Generate new session ID to invalidate all existing chat room connections
const newSessionId = `session-${Date.now()}-${
Math.random().toString(36).slice(2)
}`;
sessionId.set(newSessionId);
messages.set([]);
users.set([]);
console.log(
"[resetLobby] Chat state reset complete, new session:",
newSessionId,
);
});
// Handler for joining the chat
const joinChat = handler<
unknown,
{
chatName: string;
messages: Cell;
users: Cell;
sessionId: Cell;
nameInput: Cell;
}
>((_event, { messages, users, sessionId, nameInput }) => {
const name = nameInput.get().trim();
if (!name) {
console.log("[joinChat] No name entered, returning");
return;
}
console.log("[joinChat] Name:", name);
// Initialize session ID if not set (first user joining)
let currentSessionId = sessionId.get();
if (!currentSessionId) {
currentSessionId = `session-${Date.now()}-${
Math.random().toString(36).slice(2)
}`;
sessionId.set(currentSessionId);
console.log("[joinChat] Initialized new session:", currentSessionId);
}
// Get existing users
const existingUsers = users.get() || [];
// Check if user already exists
const existingUser = existingUsers.find((u) => u.name === name);
if (!existingUser) {
// Create new user and add to list
const newUser: User = {
name,
joinedAt: Date.now(),
color: getRandomColor(),
};
users.set([...existingUsers, newUser]);
console.log("[joinChat] User added:", name);
// Add system message for join
const existingMessages = messages.get() || [];
messages.set([
...existingMessages,
{
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
author: "System",
content: `${name} joined the chat`,
timestamp: Date.now(),
type: "system",
reactions: [],
},
]);
}
// Clear the name input
nameInput.set("");
// Create chat room instance and navigate
// Pass both the session ID at join time (mySessionId) and the Cell reference to check against (currentSessionId)
console.log(
"[joinChat] Navigating to chat room with session:",
currentSessionId,
);
const roomInstance = GroupChatRoom({
messages,
users,
myName: name,
mySessionId: currentSessionId,
currentSessionId: sessionId,
});
return navigateTo(roomInstance);
});
export default pattern(
({ chatName, messages, users, sessionId }) => {
// Name input for new users
const nameInput = Cell.of("");
// Note: Use direct property access to avoid transformer bug
// with || [] fallback (see computed-var-then-map.issue.md)
const userCount = computed(() => users.get().length);
return {
[NAME]: computed(() => `${chatName} - Lobby`),
[UI]: (
{chatName}
Enter your name to join the conversation
{/* Join Form Card */}
Join as
{/* Active Users Section */}
{ifElse(
userCount,
Now chatting
{users.map((user) => (
{computed(() => user ? getInitials(user.name) : "?")}