Improved name change

This commit is contained in:
James Ketr 2025-09-16 13:14:09 -07:00
parent c270c522f3
commit e96bd887ab
4 changed files with 185 additions and 115 deletions

View File

@ -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<LobbyProps> = (props: LobbyProps) => {
const { session, setSession, setError } = props;
const { lobbyName = "default" } = useParams<{ lobbyName: string }>();
const [lobby, setLobby] = useState<Lobby | null>(null);
const [editName, setEditName] = useState<string>("");
const [editPassword, setEditPassword] = useState<string>("");
const [socketUrl, setSocketUrl] = useState<string | null>(null);
const [creatingLobby, setCreatingLobby] = useState<boolean>(false);
const [reconnectAttempt, setReconnectAttempt] = useState<number>(0);
@ -185,24 +184,6 @@ const LobbyView: React.FC<LobbyProps> = (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<HTMLTextAreaElement>): void => {
if (event.key === "Enter") {
event.preventDefault();
const newName = editName.trim();
if (!newName || session?.name === newName) {
return;
}
setName(newName);
setEditName("");
}
};
return (
<Paper
@ -228,47 +209,13 @@ const LobbyView: React.FC<LobbyProps> = (props: LobbyProps) => {
</Typography>
</Box>
<Box>
{session.name && <Typography>You are logged in as: {session.name}</Typography>}
{!session.name && (
<Box sx={{ gap: 1, display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
<Typography>Enter your name to join:</Typography>
<Typography variant="caption">
You can optionally set a password to reserve this name; supply it again to takeover the name from
another client.
</Typography>
<Box sx={{ display: "flex", gap: 1, width: "100%", flexDirection: { xs: "column", sm: "row" } }}>
<Input
type="text"
value={editName}
onChange={(e): void => {
setEditName(e.target.value);
}}
onKeyDown={handleKeyDown}
placeholder="Your name"
/>
<Input
type="password"
value={editPassword}
onChange={(e): void => setEditPassword(e.target.value)}
placeholder="Optional password"
/>
<Tooltip title="Optional: choose a short password to reserve this name. Keep it secret.">
<span />
</Tooltip>
<Button
variant="contained"
onClick={() => {
setName(editName);
setEditName("");
setEditPassword("");
}}
disabled={!editName.trim()}
>
Join
</Button>
</Box>
</Box>
)}
<NameSetter
session={session}
sendJsonMessage={sendJsonMessage}
onNameSet={() => {
// Optional: handle any post-name-set logic
}}
/>
</Box>
</Box>
@ -349,7 +296,15 @@ const App = () => {
}, [session, getSession]);
return (
<Box sx={{ p: { xs: 1, sm: 2 }, maxWidth: { xs: "100%", sm: 800 }, margin: "0 auto", height: "100vh", overflowY: "auto" }}>
<Box
sx={{
p: { xs: 1, sm: 2 },
maxWidth: { xs: "100%", sm: 800 },
margin: "0 auto",
height: "100vh",
overflowY: "auto",
}}
>
{!session && (
<ConnectionStatus
readyState={sessionRetryAttempt > 0 ? ReadyState.CLOSED : ReadyState.CONNECTING}

View File

@ -1589,7 +1589,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
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<MediaControlProps> = ({
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<MediaControlProps> = ({
<Moveable
ref={moveableRef}
flushSync={flushSync}
container={containerRef.current} // Constrain to container if needed
container={containerRef.current}
pinchable={true}
draggable={true}
target={targetRef.current}
@ -1702,7 +1702,6 @@ const MediaControl: React.FC<MediaControlProps> = ({
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<MediaControlProps> = ({
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<MediaControlProps> = ({
}}
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<MediaControlProps> = ({
setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
}
}
// Reset spacer border color
if (spacerRef.current) {
spacerRef.current.style.borderColor = "#666";
}

162
client/src/NameSetter.tsx Normal file
View File

@ -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<NameSetterProps> = ({
session,
sendJsonMessage,
onNameSet,
initialName = "",
initialPassword = "",
}) => {
const [editName, setEditName] = useState<string>(initialName);
const [editPassword, setEditPassword] = useState<string>(initialPassword);
const [showDialog, setShowDialog] = useState<boolean>(!session.name);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const nameInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>): void => {
if (event.key === "Enter") {
event.preventDefault();
if (passwordInputRef.current) {
passwordInputRef.current.focus();
}
}
};
const handlePasswordKeyDown = (event: KeyboardEvent<HTMLInputElement>): 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 (
<Box sx={{ gap: 1, display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
{session.name && !showDialog && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Typography>You are logged in as: {session.name}</Typography>
<Button variant="outlined" size="small" onClick={handleOpenDialog}>
Change Name
</Button>
</Box>
)}
{/* Dialog for name change */}
<Dialog open={showDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{session.name ? "Change Your Name" : "Enter Your Name"}
</DialogTitle>
<DialogContent>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, pt: 1 }}>
<Typography variant="body2" color="text.secondary">
{session.name
? "Enter a new name to change your current name."
: "Enter your name to join the lobby."
}
</Typography>
<Typography variant="caption" color="text.secondary">
You can optionally set a password to reserve this name; supply it again to takeover the name from another client.
</Typography>
<Input
inputRef={nameInputRef}
type="text"
value={editName}
onChange={(e): void => {
setEditName(e.target.value);
}}
onKeyDown={handleNameKeyDown}
placeholder="Your name"
fullWidth
autoFocus
/>
<Input
inputRef={passwordInputRef}
type="password"
value={editPassword}
onChange={(e): void => setEditPassword(e.target.value)}
onKeyDown={handlePasswordKeyDown}
placeholder="Optional password"
fullWidth
/>
<Tooltip title="Optional: choose a short password to reserve this name. Keep it secret.">
<span />
</Tooltip>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog} disabled={isSubmitting}>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSubmit}
disabled={!canSubmit}
color={hasNameChanged ? "primary" : "inherit"}
>
{isSubmitting ? "Changing..." : (session.name ? "Change Name" : "Join")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default NameSetter;

View File

@ -184,19 +184,6 @@ const UserList: React.FC<UserListProps> = (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 (
<Box sx={{ position: "relative", width: "100%" }}>
<Paper
@ -207,27 +194,6 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
m: { xs: 0, sm: 2 },
}}
>
{/* Name change UI for local user */}
{session && (
<Box sx={{ mb: 2, display: "flex", alignItems: "center", gap: 1 }}>
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="Change your name"
style={{ fontSize: "1em", padding: "4px 8px", borderRadius: 4, border: "1px solid #ccc" }}
disabled={changingName}
/>
<Button
variant="contained"
size="small"
onClick={handleChangeName}
disabled={changingName || !newName.trim()}
>
{changingName ? "Changing..." : "Change Name"}
</Button>
</Box>
)}
<MediaAgent {...{ session, socketUrl, peers, setPeers }} />
<List className="UserSelector">
{users?.map((user) => (