P14d: guest display names for players
- Players set a display name in the session panel ("Your name"); it persists
(sessionStore.guestName) and is sent via a new `setName` message on join + edit.
- Server stores the name per connection and uses it in the roster + chat; the roster
entry now also carries the player's character, so the GM sees "Alice · Lia".
- Realtime e2e: a player names themselves and the GM's roster/share update to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { useSessionStore } from '@/stores/sessionStore';
|
||||
import { usePlayerSessionStore } from '@/stores/playerSessionStore';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
import { sendChat, sendPrivateHandout } from '@/lib/sync/wsSync';
|
||||
import { sendChat, sendPrivateHandout, sendSetName } from '@/lib/sync/wsSync';
|
||||
import { sessionLogRepo } from '@/lib/db/repositories';
|
||||
import type { SessionLogEntry } from '@/lib/schemas';
|
||||
import type { RosterEntry } from '@/lib/sync/messages';
|
||||
@@ -25,6 +25,8 @@ export function SessionSidebar({ open, onClose }: { open: boolean; onClose: () =
|
||||
const joinCode = useSessionStore((s) => s.joinCode);
|
||||
const intent = useSessionStore((s) => s.joinIntent);
|
||||
const roster = useSessionStore((s) => s.roster);
|
||||
const guestName = useSessionStore((s) => s.guestName);
|
||||
const setGuestName = useSessionStore((s) => s.setGuestName);
|
||||
const rolls = usePlayerSessionStore((s) => s.rolls);
|
||||
const chat = usePlayerSessionStore((s) => s.chat);
|
||||
const activeCampaignId = useUiStore((s) => s.activeCampaignId);
|
||||
@@ -34,6 +36,8 @@ export function SessionSidebar({ open, onClose }: { open: boolean; onClose: () =
|
||||
const inSession = role !== 'off' || !!intent;
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'rolls' | 'recap'>('chat');
|
||||
const [nameDraft, setNameDraft] = useState(guestName);
|
||||
const commitName = () => { const n = nameDraft.trim(); setGuestName(n); sendSetName(n); };
|
||||
const [draft, setDraft] = useState('');
|
||||
const [whisperTo, setWhisperTo] = useState(''); // GM: playerId; player: 'gm' to whisper the GM
|
||||
const targets = roster.filter((p) => p.playerId !== 'gm');
|
||||
@@ -89,6 +93,13 @@ export function SessionSidebar({ open, onClose }: { open: boolean; onClose: () =
|
||||
<p className="p-3 text-sm text-muted">You're not in a live session. Host one (GM) or open a join link to play together.</p>
|
||||
)}
|
||||
|
||||
{inSession && !isGm && (
|
||||
<div className="border-b border-line p-3">
|
||||
<label className="text-xs font-semibold uppercase tracking-wide text-muted">Your name</label>
|
||||
<Input value={nameDraft} onChange={(e) => setNameDraft(e.target.value)} onBlur={commitName} onKeyDown={(e) => { if (e.key === 'Enter') commitName(); }} placeholder="What should we call you?" aria-label="Your display name" className="mt-1" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inSession && (
|
||||
<div className="border-b border-line p-3">
|
||||
<h3 className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted">Who's here ({roster.length})</h3>
|
||||
@@ -96,7 +107,7 @@ export function SessionSidebar({ open, onClose }: { open: boolean; onClose: () =
|
||||
{roster.map((p) => (
|
||||
<li key={p.playerId} className={cn('flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs', p.playerId === 'gm' ? 'border-accent/50 text-accent' : 'border-line text-ink')}>
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-success" aria-hidden />
|
||||
{p.name}
|
||||
{p.name}{p.character ? <span className="text-muted"> · {p.character}</span> : null}
|
||||
{isGm && p.playerId !== 'gm' && (
|
||||
<button onClick={() => openShare(p)} title={`Share privately with ${p.name}`} aria-label={`Share with ${p.name}`} className="ml-0.5 text-muted hover:text-info">📨</button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user