diff --git a/client/src/App.tsx b/client/src/App.tsx index 1edb65a..56dfdac 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, KeyboardEvent, useCallback } from "react"; -import { Input, Paper, Typography } from "@mui/material"; +import React, { useState, useEffect, useCallback } from "react"; +import { Paper, Typography } from "@mui/material"; import { Session, Lobby } from "./GlobalContext"; import { UserList } from "./UserList"; @@ -7,11 +7,12 @@ import { LobbyChat } from "./LobbyChat"; import BotManager from "./BotManager"; import "./App.css"; import { ws_base, base } from "./Common"; -import { Box, Button, Tooltip } from "@mui/material"; +import { Box } from "@mui/material"; import { BrowserRouter as Router, Route, Routes, useParams } from "react-router-dom"; import useWebSocket, { ReadyState } from "react-use-websocket"; import ConnectionStatus from "./ConnectionStatus"; import { sessionApi, LobbyCreateRequest } from "./api-client"; +import NameSetter from "./NameSetter"; console.log(`AI Voice Chat Build: ${process.env.REACT_APP_AI_VOICECHAT_BUILD}`); @@ -25,8 +26,6 @@ const LobbyView: React.FC = (props: LobbyProps) => { const { session, setSession, setError } = props; const { lobbyName = "default" } = useParams<{ lobbyName: string }>(); const [lobby, setLobby] = useState(null); - const [editName, setEditName] = useState(""); - const [editPassword, setEditPassword] = useState(""); const [socketUrl, setSocketUrl] = useState(null); const [creatingLobby, setCreatingLobby] = useState(false); const [reconnectAttempt, setReconnectAttempt] = useState(0); @@ -185,24 +184,6 @@ const LobbyView: React.FC = (props: LobbyProps) => { setCreatingLobby(false); }); }, [session, lobbyName, lobby, setLobby, setError, creatingLobby]); - const setName = (name: string) => { - sendJsonMessage({ - type: "set_name", - data: { name, password: editPassword ? editPassword : undefined }, - }); - }; - - const handleKeyDown = (event: KeyboardEvent): void => { - if (event.key === "Enter") { - event.preventDefault(); - const newName = editName.trim(); - if (!newName || session?.name === newName) { - return; - } - setName(newName); - setEditName(""); - } - }; return ( = (props: LobbyProps) => { - {session.name && You are logged in as: {session.name}} - {!session.name && ( - - Enter your name to join: - - You can optionally set a password to reserve this name; supply it again to takeover the name from - another client. - - - { - setEditName(e.target.value); - }} - onKeyDown={handleKeyDown} - placeholder="Your name" - /> - setEditPassword(e.target.value)} - placeholder="Optional password" - /> - - - - - - - )} + { + // Optional: handle any post-name-set logic + }} + /> @@ -349,7 +296,15 @@ const App = () => { }, [session, getSession]); return ( - + {!session && ( 0 ? ReadyState.CLOSED : ReadyState.CONNECTING} diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index 62e408b..8d077c0 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -1589,7 +1589,7 @@ const MediaControl: React.FC = ({ ref={containerRef} className="MediaControlContainer" style={{ - position: "relative", // Ensure this is set inline too + position: "relative", width: "max-content", height: "max-content", }} @@ -1626,7 +1626,7 @@ const MediaControl: React.FC = ({ className={`MediaControl ${className}`} data-peer={peer.session_id} style={{ - position: "absolute", // Ensure this is set + position: "absolute", top: "0px", left: "0px", width: frame.width ? `${frame.width}px` : undefined, @@ -1690,7 +1690,7 @@ const MediaControl: React.FC = ({ = ({ origin={false} edge onDragStart={(e) => { - // Only block drag if the event is a pointerdown/touchstart on a button, but do not interfere with click/touch events const controls = containerRef.current?.querySelector(".Controls"); const target = e.inputEvent?.target as HTMLElement; if (controls && target && (target.closest("button") || target.closest(".MuiIconButton-root"))) { @@ -1717,13 +1716,9 @@ const MediaControl: React.FC = ({ if (targetRef.current) { targetRef.current.style.transform = e.transform; } - - // Check for snap-back const matrix = new DOMMatrix(e.transform); const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); - if (shouldSnap && spacerRef.current) { - // Add visual feedback for snap zone spacerRef.current.style.borderColor = "#0088ff"; } else if (spacerRef.current) { spacerRef.current.style.borderColor = "#666"; @@ -1731,21 +1726,15 @@ const MediaControl: React.FC = ({ }} onDragEnd={(e) => { setIsDragging(false); - if (targetRef.current) { const computedStyle = getComputedStyle(targetRef.current); const transform = computedStyle.transform; - if (transform && transform !== "none") { const matrix = new DOMMatrix(transform); const shouldSnap = checkSnapBack(matrix.m41, matrix.m42); - if (shouldSnap) { - // Snap back to origin targetRef.current.style.transform = "translate(0px, 0px)"; setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); - - // Reset size if needed if (spacerRef.current) { const spacerRect = spacerRef.current.getBoundingClientRect(); targetRef.current.style.width = `${spacerRect.width}px`; @@ -1763,8 +1752,6 @@ const MediaControl: React.FC = ({ setFrame({ translate: [0, 0], width: frame.width, height: frame.height }); } } - - // Reset spacer border color if (spacerRef.current) { spacerRef.current.style.borderColor = "#666"; } diff --git a/client/src/NameSetter.tsx b/client/src/NameSetter.tsx new file mode 100644 index 0000000..9ff0bb3 --- /dev/null +++ b/client/src/NameSetter.tsx @@ -0,0 +1,162 @@ +import React, { useState, KeyboardEvent, useRef } from "react"; +import { Input, Button, Box, Typography, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material"; +import { Session } from "./GlobalContext"; + +interface NameSetterProps { + session: Session; + sendJsonMessage: (message: any) => void; + onNameSet?: () => void; + initialName?: string; + initialPassword?: string; +} + +const NameSetter: React.FC = ({ + session, + sendJsonMessage, + onNameSet, + initialName = "", + initialPassword = "", +}) => { + const [editName, setEditName] = useState(initialName); + const [editPassword, setEditPassword] = useState(initialPassword); + const [showDialog, setShowDialog] = useState(!session.name); + const [isSubmitting, setIsSubmitting] = useState(false); + + const nameInputRef = useRef(null); + const passwordInputRef = useRef(null); + + const setName = (name: string) => { + setIsSubmitting(true); + sendJsonMessage({ + type: "set_name", + data: { name, password: editPassword ? editPassword : undefined }, + }); + if (onNameSet) { + onNameSet(); + } + setShowDialog(false); + setIsSubmitting(false); + setEditName(""); + setEditPassword(""); + }; + + const handleNameKeyDown = (event: KeyboardEvent): void => { + if (event.key === "Enter") { + event.preventDefault(); + if (passwordInputRef.current) { + passwordInputRef.current.focus(); + } + } + }; + + const handlePasswordKeyDown = (event: KeyboardEvent): void => { + if (event.key === "Enter") { + event.preventDefault(); + handleSubmit(); + } + }; + + const handleSubmit = () => { + const newName = editName.trim(); + if (!newName || (session?.name && session.name === newName)) { + return; + } + setName(newName); + }; + + const handleOpenDialog = () => { + setEditName(session.name || ""); + setEditPassword(""); + setShowDialog(true); + // Focus the name input when dialog opens + setTimeout(() => { + if (nameInputRef.current) { + nameInputRef.current.focus(); + } + }, 100); + }; + + const handleCloseDialog = () => { + setShowDialog(false); + setEditName(""); + setEditPassword(""); + }; + + const hasNameChanged = editName.trim() !== (session.name || ""); + const canSubmit = editName.trim() && hasNameChanged && !isSubmitting; + + return ( + + {session.name && !showDialog && ( + + You are logged in as: {session.name} + + + )} + + {/* Dialog for name change */} + + + {session.name ? "Change Your Name" : "Enter Your Name"} + + + + + {session.name + ? "Enter a new name to change your current name." + : "Enter your name to join the lobby." + } + + + You can optionally set a password to reserve this name; supply it again to takeover the name from another client. + + + { + setEditName(e.target.value); + }} + onKeyDown={handleNameKeyDown} + placeholder="Your name" + fullWidth + autoFocus + /> + + setEditPassword(e.target.value)} + onKeyDown={handlePasswordKeyDown} + placeholder="Optional password" + fullWidth + /> + + + + + + + + + + + + + ); +}; + +export default NameSetter; \ No newline at end of file diff --git a/client/src/UserList.tsx b/client/src/UserList.tsx index 81935ea..f67078b 100644 --- a/client/src/UserList.tsx +++ b/client/src/UserList.tsx @@ -184,19 +184,6 @@ const UserList: React.FC = (props: UserListProps) => { }); }, [users, sendJsonMessage]); - // Add state for name change UI - const [newName, setNewName] = useState(""); - const [changingName, setChangingName] = useState(false); - - // Handler for changing name - const handleChangeName = () => { - if (!newName.trim()) return; - setChangingName(true); - sendJsonMessage({ type: "set_name", data: { name: newName } }); - setNewName(""); - setTimeout(() => setChangingName(false), 1000); // UI feedback - }; - return ( = (props: UserListProps) => { m: { xs: 0, sm: 2 }, }} > - {/* Name change UI for local user */} - {session && ( - - setNewName(e.target.value)} - placeholder="Change your name" - style={{ fontSize: "1em", padding: "4px 8px", borderRadius: 4, border: "1px solid #ccc" }} - disabled={changingName} - /> - - - )} {users?.map((user) => (