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 { 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}
|
||||
|
@ -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
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]);
|
||||
|
||||
// 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) => (
|
||||
|
Loading…
x
Reference in New Issue
Block a user