258 lines
8.7 KiB
TypeScript
258 lines
8.7 KiB
TypeScript
import React, { useState, useEffect, useCallback } from "react";
|
|
import Paper from "@mui/material/Paper";
|
|
import List from "@mui/material/List";
|
|
import Button from "@mui/material/Button";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import Dialog from "@mui/material/Dialog";
|
|
import DialogTitle from "@mui/material/DialogTitle";
|
|
import DialogContent from "@mui/material/DialogContent";
|
|
import DialogActions from "@mui/material/DialogActions";
|
|
import SettingsIcon from "@mui/icons-material/Settings";
|
|
import "./UserList.css";
|
|
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
|
import Box from "@mui/material/Box";
|
|
import { Session } from "./GlobalContext";
|
|
import useWebSocket from "react-use-websocket";
|
|
import { ApiClient } from "./api-client";
|
|
import BotConfig from "./BotConfig";
|
|
|
|
type User = {
|
|
name: string;
|
|
session_id: string;
|
|
live: boolean;
|
|
local: boolean /* Client side variable */;
|
|
protected?: boolean;
|
|
has_media?: boolean; // Whether this user provides audio/video streams
|
|
bot_run_id?: string;
|
|
bot_provider_id?: string;
|
|
bot_instance_id?: string; // For bot instances
|
|
};
|
|
|
|
type UserListProps = {
|
|
socketUrl: string;
|
|
session: Session;
|
|
lobbyId: string;
|
|
};
|
|
|
|
const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|
const { socketUrl, session, lobbyId } = props;
|
|
const [users, setUsers] = useState<User[] | null>(null);
|
|
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
|
const [videoClass, setVideoClass] = useState<string>("Large");
|
|
|
|
// Bot configuration state
|
|
const [botConfigDialogOpen, setBotConfigDialogOpen] = useState(false);
|
|
const [selectedBotForConfig, setSelectedBotForConfig] = useState<User | null>(null);
|
|
const [leavingBots, setLeavingBots] = useState<Set<string>>(new Set());
|
|
|
|
const apiClient = new ApiClient();
|
|
|
|
const handleBotLeave = async (user: User) => {
|
|
if (!user.bot_instance_id) return;
|
|
|
|
setLeavingBots((prev) => new Set(prev).add(user.session_id));
|
|
|
|
try {
|
|
await apiClient.createRequestBotLeaveLobby(user.bot_instance_id);
|
|
console.log(`Bot ${user.name} leave requested successfully`);
|
|
} catch (error) {
|
|
console.error("Failed to request bot leave:", error);
|
|
} finally {
|
|
setLeavingBots((prev) => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(user.session_id);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
// Bot configuration handlers
|
|
const handleOpenBotConfig = (user: User) => {
|
|
setSelectedBotForConfig(user);
|
|
setBotConfigDialogOpen(true);
|
|
};
|
|
|
|
const handleCloseBotConfig = () => {
|
|
setBotConfigDialogOpen(false);
|
|
setSelectedBotForConfig(null);
|
|
};
|
|
|
|
const sortUsers = useCallback(
|
|
(A: any, B: any) => {
|
|
if (!session) {
|
|
return 0;
|
|
}
|
|
/* active user first */
|
|
if (A.name === session.name) {
|
|
return -1;
|
|
}
|
|
if (B.name === session.name) {
|
|
return +1;
|
|
}
|
|
/* Sort active users first */
|
|
if (A.name && !B.name) {
|
|
return -1;
|
|
}
|
|
if (B.name && !A.name) {
|
|
return +1;
|
|
}
|
|
/* Otherwise, sort by color */
|
|
if (A.color && B.color) {
|
|
return A.color.localeCompare(B.color);
|
|
}
|
|
return 0;
|
|
},
|
|
[session]
|
|
);
|
|
|
|
// Use the WebSocket hook for lobby events with automatic reconnection
|
|
const { sendJsonMessage } = useWebSocket(socketUrl, {
|
|
share: true,
|
|
shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss
|
|
reconnectInterval: 5000,
|
|
onMessage: (event: MessageEvent) => {
|
|
if (!session) {
|
|
return;
|
|
}
|
|
const message = JSON.parse(event.data);
|
|
const data: any = message.data;
|
|
switch (message.type) {
|
|
case "lobby_state":
|
|
type LobbyStateData = {
|
|
participants: User[];
|
|
};
|
|
const lobby_state = data as LobbyStateData;
|
|
console.log(`users - lobby_state`, lobby_state.participants);
|
|
lobby_state.participants.forEach((user) => {
|
|
user.local = user.session_id === session.id;
|
|
});
|
|
lobby_state.participants.sort(sortUsers);
|
|
setVideoClass(lobby_state.participants.length <= 2 ? "Medium" : "Small");
|
|
setUsers(lobby_state.participants);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (users !== null) {
|
|
return;
|
|
}
|
|
sendJsonMessage({
|
|
type: "list_users",
|
|
});
|
|
}, [users, sendJsonMessage]);
|
|
|
|
return (
|
|
<Paper className={`UserList ${videoClass}`}>
|
|
<MediaAgent {...{ session, socketUrl, peers, setPeers }} />
|
|
<List className="UserSelector">
|
|
{users?.map((user) => (
|
|
<Box
|
|
key={user.session_id}
|
|
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
|
|
className={`UserEntry ${user.local ? "UserSelf" : ""}`}
|
|
>
|
|
<Box>
|
|
<Box style={{ display: "flex-wrap", alignItems: "center", justifyContent: "space-between" }}>
|
|
<Box style={{ display: "flex-wrap", alignItems: "center" }}>
|
|
<div className="Name">{user.name ? user.name : user.session_id}</div>
|
|
{user.protected && (
|
|
<div
|
|
style={{ marginLeft: 8, fontSize: "0.8em", color: "#a00" }}
|
|
title="This name is protected with a password"
|
|
>
|
|
🔒
|
|
</div>
|
|
)}
|
|
{user.bot_instance_id && (
|
|
<div style={{ marginLeft: 8, fontSize: "0.8em", color: "#00a" }} title="This is a bot">
|
|
🤖
|
|
</div>
|
|
)}
|
|
</Box>
|
|
{user.bot_instance_id && (
|
|
<Paper elevation={2} sx={{ display: "flex-wrap", gap: "4px", p: 1 }}>
|
|
{user.bot_run_id && (
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => handleOpenBotConfig(user)}
|
|
style={{ width: "24px", height: "24px", fontSize: "0.7em" }}
|
|
title="Configure bot"
|
|
>
|
|
<SettingsIcon style={{ fontSize: "14px" }} />
|
|
</IconButton>
|
|
)}
|
|
<Button
|
|
size="small"
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => handleBotLeave(user)}
|
|
disabled={leavingBots.has(user.session_id) || !user.bot_instance_id}
|
|
style={{ fontSize: "0.7em", minWidth: "50px", height: "24px" }}
|
|
title={!user.bot_instance_id ? "Bot instance ID not available" : "Remove bot from lobby"}
|
|
>
|
|
{leavingBots.has(user.session_id) ? "..." : "Leave"}
|
|
</Button>
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
{user.name && !user.live && <div className="NoNetwork"></div>}
|
|
</Box>
|
|
{user.name && user.live && peers[user.session_id] && (user.local || user.has_media !== false) ? (
|
|
<MediaControl
|
|
className={videoClass}
|
|
key={user.session_id}
|
|
peer={peers[user.session_id]}
|
|
isSelf={user.local}
|
|
/>
|
|
) : user.name && user.live && user.has_media === false ? (
|
|
<div
|
|
className="Video fade-in"
|
|
style={{
|
|
background: "#333",
|
|
color: "#fff",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: "100%",
|
|
height: "100%",
|
|
fontSize: "14px",
|
|
}}
|
|
>
|
|
💬 Chat Only
|
|
</div>
|
|
) : (
|
|
<video className="Video"></video>
|
|
)}
|
|
</Box>
|
|
))}
|
|
</List>
|
|
|
|
{/* Bot Configuration Dialog */}
|
|
<Dialog open={botConfigDialogOpen} onClose={handleCloseBotConfig} maxWidth="md" fullWidth>
|
|
<DialogTitle>Configure Bot</DialogTitle>
|
|
<DialogContent>
|
|
{selectedBotForConfig && (
|
|
<BotConfig
|
|
botName={selectedBotForConfig.name?.replace(/-bot$/, "") || "unknown"}
|
|
lobbyId={lobbyId}
|
|
onConfigUpdate={(config) => {
|
|
console.log("Bot configuration updated:", config);
|
|
// Configuration updates are handled via WebSocket, so we don't need to do anything special here
|
|
}}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleCloseBotConfig}>Close</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
export { UserList };
|