Improved name change
This commit is contained in:
parent
c270c522f3
commit
e96bd887ab
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, KeyboardEvent, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Input, Paper, Typography } from "@mui/material";
|
import { Paper, Typography } from "@mui/material";
|
||||||
|
|
||||||
import { Session, Lobby } from "./GlobalContext";
|
import { Session, Lobby } from "./GlobalContext";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
@ -7,11 +7,12 @@ import { LobbyChat } from "./LobbyChat";
|
|||||||
import BotManager from "./BotManager";
|
import BotManager from "./BotManager";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { ws_base, base } from "./Common";
|
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 { BrowserRouter as Router, Route, Routes, useParams } from "react-router-dom";
|
||||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||||
import ConnectionStatus from "./ConnectionStatus";
|
import ConnectionStatus from "./ConnectionStatus";
|
||||||
import { sessionApi, LobbyCreateRequest } from "./api-client";
|
import { sessionApi, LobbyCreateRequest } from "./api-client";
|
||||||
|
import NameSetter from "./NameSetter";
|
||||||
|
|
||||||
console.log(`AI Voice Chat Build: ${process.env.REACT_APP_AI_VOICECHAT_BUILD}`);
|
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 { session, setSession, setError } = props;
|
||||||
const { lobbyName = "default" } = useParams<{ lobbyName: string }>();
|
const { lobbyName = "default" } = useParams<{ lobbyName: string }>();
|
||||||
const [lobby, setLobby] = useState<Lobby | null>(null);
|
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 [socketUrl, setSocketUrl] = useState<string | null>(null);
|
||||||
const [creatingLobby, setCreatingLobby] = useState<boolean>(false);
|
const [creatingLobby, setCreatingLobby] = useState<boolean>(false);
|
||||||
const [reconnectAttempt, setReconnectAttempt] = useState<number>(0);
|
const [reconnectAttempt, setReconnectAttempt] = useState<number>(0);
|
||||||
@ -185,24 +184,6 @@ const LobbyView: React.FC<LobbyProps> = (props: LobbyProps) => {
|
|||||||
setCreatingLobby(false);
|
setCreatingLobby(false);
|
||||||
});
|
});
|
||||||
}, [session, lobbyName, lobby, setLobby, setError, creatingLobby]);
|
}, [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 (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@ -228,47 +209,13 @@ const LobbyView: React.FC<LobbyProps> = (props: LobbyProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
{session.name && <Typography>You are logged in as: {session.name}</Typography>}
|
<NameSetter
|
||||||
{!session.name && (
|
session={session}
|
||||||
<Box sx={{ gap: 1, display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
|
sendJsonMessage={sendJsonMessage}
|
||||||
<Typography>Enter your name to join:</Typography>
|
onNameSet={() => {
|
||||||
<Typography variant="caption">
|
// Optional: handle any post-name-set logic
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -349,7 +296,15 @@ const App = () => {
|
|||||||
}, [session, getSession]);
|
}, [session, getSession]);
|
||||||
|
|
||||||
return (
|
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 && (
|
{!session && (
|
||||||
<ConnectionStatus
|
<ConnectionStatus
|
||||||
readyState={sessionRetryAttempt > 0 ? ReadyState.CLOSED : ReadyState.CONNECTING}
|
readyState={sessionRetryAttempt > 0 ? ReadyState.CLOSED : ReadyState.CONNECTING}
|
||||||
|
@ -1589,7 +1589,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="MediaControlContainer"
|
className="MediaControlContainer"
|
||||||
style={{
|
style={{
|
||||||
position: "relative", // Ensure this is set inline too
|
position: "relative",
|
||||||
width: "max-content",
|
width: "max-content",
|
||||||
height: "max-content",
|
height: "max-content",
|
||||||
}}
|
}}
|
||||||
@ -1626,7 +1626,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
className={`MediaControl ${className}`}
|
className={`MediaControl ${className}`}
|
||||||
data-peer={peer.session_id}
|
data-peer={peer.session_id}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute", // Ensure this is set
|
position: "absolute",
|
||||||
top: "0px",
|
top: "0px",
|
||||||
left: "0px",
|
left: "0px",
|
||||||
width: frame.width ? `${frame.width}px` : undefined,
|
width: frame.width ? `${frame.width}px` : undefined,
|
||||||
@ -1690,7 +1690,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
<Moveable
|
<Moveable
|
||||||
ref={moveableRef}
|
ref={moveableRef}
|
||||||
flushSync={flushSync}
|
flushSync={flushSync}
|
||||||
container={containerRef.current} // Constrain to container if needed
|
container={containerRef.current}
|
||||||
pinchable={true}
|
pinchable={true}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
target={targetRef.current}
|
target={targetRef.current}
|
||||||
@ -1702,7 +1702,6 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
origin={false}
|
origin={false}
|
||||||
edge
|
edge
|
||||||
onDragStart={(e) => {
|
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 controls = containerRef.current?.querySelector(".Controls");
|
||||||
const target = e.inputEvent?.target as HTMLElement;
|
const target = e.inputEvent?.target as HTMLElement;
|
||||||
if (controls && target && (target.closest("button") || target.closest(".MuiIconButton-root"))) {
|
if (controls && target && (target.closest("button") || target.closest(".MuiIconButton-root"))) {
|
||||||
@ -1717,13 +1716,9 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
if (targetRef.current) {
|
if (targetRef.current) {
|
||||||
targetRef.current.style.transform = e.transform;
|
targetRef.current.style.transform = e.transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for snap-back
|
|
||||||
const matrix = new DOMMatrix(e.transform);
|
const matrix = new DOMMatrix(e.transform);
|
||||||
const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
|
const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
|
||||||
|
|
||||||
if (shouldSnap && spacerRef.current) {
|
if (shouldSnap && spacerRef.current) {
|
||||||
// Add visual feedback for snap zone
|
|
||||||
spacerRef.current.style.borderColor = "#0088ff";
|
spacerRef.current.style.borderColor = "#0088ff";
|
||||||
} else if (spacerRef.current) {
|
} else if (spacerRef.current) {
|
||||||
spacerRef.current.style.borderColor = "#666";
|
spacerRef.current.style.borderColor = "#666";
|
||||||
@ -1731,21 +1726,15 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
|||||||
}}
|
}}
|
||||||
onDragEnd={(e) => {
|
onDragEnd={(e) => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
if (targetRef.current) {
|
if (targetRef.current) {
|
||||||
const computedStyle = getComputedStyle(targetRef.current);
|
const computedStyle = getComputedStyle(targetRef.current);
|
||||||
const transform = computedStyle.transform;
|
const transform = computedStyle.transform;
|
||||||
|
|
||||||
if (transform && transform !== "none") {
|
if (transform && transform !== "none") {
|
||||||
const matrix = new DOMMatrix(transform);
|
const matrix = new DOMMatrix(transform);
|
||||||
const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
|
const shouldSnap = checkSnapBack(matrix.m41, matrix.m42);
|
||||||
|
|
||||||
if (shouldSnap) {
|
if (shouldSnap) {
|
||||||
// Snap back to origin
|
|
||||||
targetRef.current.style.transform = "translate(0px, 0px)";
|
targetRef.current.style.transform = "translate(0px, 0px)";
|
||||||
setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
|
setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
|
||||||
|
|
||||||
// Reset size if needed
|
|
||||||
if (spacerRef.current) {
|
if (spacerRef.current) {
|
||||||
const spacerRect = spacerRef.current.getBoundingClientRect();
|
const spacerRect = spacerRef.current.getBoundingClientRect();
|
||||||
targetRef.current.style.width = `${spacerRect.width}px`;
|
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 });
|
setFrame({ translate: [0, 0], width: frame.width, height: frame.height });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset spacer border color
|
|
||||||
if (spacerRef.current) {
|
if (spacerRef.current) {
|
||||||
spacerRef.current.style.borderColor = "#666";
|
spacerRef.current.style.borderColor = "#666";
|
||||||
}
|
}
|
||||||
|
162
client/src/NameSetter.tsx
Normal file
162
client/src/NameSetter.tsx
Normal 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;
|
@ -184,19 +184,6 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
});
|
});
|
||||||
}, [users, sendJsonMessage]);
|
}, [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 (
|
return (
|
||||||
<Box sx={{ position: "relative", width: "100%" }}>
|
<Box sx={{ position: "relative", width: "100%" }}>
|
||||||
<Paper
|
<Paper
|
||||||
@ -207,27 +194,6 @@ const UserList: React.FC<UserListProps> = (props: UserListProps) => {
|
|||||||
m: { xs: 0, sm: 2 },
|
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 }} />
|
<MediaAgent {...{ session, socketUrl, peers, setPeers }} />
|
||||||
<List className="UserSelector">
|
<List className="UserSelector">
|
||||||
{users?.map((user) => (
|
{users?.map((user) => (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user