mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
fix: notification pushes now match what the 2023 client reads
Most pushes were still shaped for the 2020 watch, so the 2023 client parsed them as empty or dropped them without any error. - RelationshipChanged carries the relationship row, to both players, on friend request / accept / remove / add and on favorite / mute / ignore. Friend requests and accepts also land in the notifications inbox. - ModerationKick uses the client's flat moderation block, with the reason in VoteKickReason (the only field the dialog shows). - Bans, account deletion, password resets and second-session takeover are a Logout with a numeric reason instead of a kick to the dorm; the old string reason failed the client's parse. - SubscribeToPlayers is honoured: each connection joins a presence group per subscribed player, and presence (only presence) reaches subscribers as well as friends. The hub allows 180 s of client silence, since the client reconnects at most 3 times per login. - Admin broadcast sends its text as a coach message and can start the client's own maintenance countdown; ServerMaintenance only reads StartsInMinutes. - ConsumableMapping, StorefrontBalance, gift, player event, level-up, RoomUpdate, profile-image and PlayerLeftChat pushes carry the client's DTOs. "Join rejected" is the client's RequestGameInvite message. Contract in docs/recroom-2023-notifications.md; push frames are checked by NotificationContractTests through a capturing hub context.
This commit is contained in:
@@ -5,18 +5,32 @@ import { useToast } from '../components/Toast';
|
||||
|
||||
export function Broadcast() {
|
||||
const [message, setMessage] = useState('');
|
||||
const [minutes, setMinutes] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const startsInMinutes = minutes.trim() === '' ? null : Number(minutes);
|
||||
const minutesValid = startsInMinutes === null || (Number.isInteger(startsInMinutes) && startsInMinutes >= 0);
|
||||
const canSend = !busy && minutesValid && (message.trim() !== '' || startsInMinutes !== null);
|
||||
|
||||
const send = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!message.trim()) return;
|
||||
if (!confirm(`Broadcast this to every connected player?\n\n"${message}"`)) return;
|
||||
if (!canSend) return;
|
||||
const parts = [
|
||||
message.trim() && `"${message.trim()}"`,
|
||||
startsInMinutes !== null &&
|
||||
(startsInMinutes === 0 ? 'Cancel the maintenance countdown' : `Maintenance countdown: ${startsInMinutes} min`),
|
||||
].filter(Boolean);
|
||||
if (!confirm(`Broadcast this to every connected player?\n\n${parts.join('\n')}`)) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await api('/broadcast', { method: 'POST', body: { Message: message } });
|
||||
toast.push('Broadcast sent', 'success');
|
||||
const res = await api<{ recipients: number }>('/broadcast', {
|
||||
method: 'POST',
|
||||
body: { Message: message.trim() || null, StartsInMinutes: startsInMinutes },
|
||||
});
|
||||
toast.push(`Broadcast sent to ${res.recipients} player(s)`, 'success');
|
||||
setMessage('');
|
||||
setMinutes('');
|
||||
} catch (e) {
|
||||
toast.push((e as Error).message, 'error');
|
||||
} finally {
|
||||
@@ -28,7 +42,7 @@ export function Broadcast() {
|
||||
<div>
|
||||
<PageHeader
|
||||
title="Broadcast"
|
||||
blurb="Push a server-maintenance notification to every connected player. The watch shows it as a system toast."
|
||||
blurb="Message every connected player. The message lands in their in-game notifications from the system account; the optional countdown starts the game's own maintenance timer."
|
||||
/>
|
||||
|
||||
<form onSubmit={send} className="card !p-5 max-w-2xl">
|
||||
@@ -39,14 +53,33 @@ export function Broadcast() {
|
||||
rows={5}
|
||||
className="input mt-1.5 font-mono text-sm"
|
||||
placeholder="Server going down for maintenance in 5 minutes — log out cleanly to avoid losing your dorm save."
|
||||
required
|
||||
/>
|
||||
<div className="mt-2 text-xs text-ink-400">
|
||||
{message.length} characters
|
||||
</div>
|
||||
|
||||
<label className="label mt-4">Maintenance countdown (minutes, optional)</label>
|
||||
<input
|
||||
value={minutes}
|
||||
onChange={e => setMinutes(e.target.value)}
|
||||
inputMode="numeric"
|
||||
className="input mt-1.5 w-40 text-sm"
|
||||
placeholder="e.g. 5"
|
||||
/>
|
||||
<div className="mt-2 text-xs text-ink-400">
|
||||
{minutesValid ? 'Leave empty for no countdown; 0 cancels a running one.' : 'Enter a whole number of minutes.'}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-2 justify-end">
|
||||
<button type="button" onClick={() => setMessage('')} className="btn-ghost text-xs" disabled={busy || !message}>Clear</button>
|
||||
<button className="btn-primary text-xs" disabled={busy || !message.trim()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setMessage(''); setMinutes(''); }}
|
||||
className="btn-ghost text-xs"
|
||||
disabled={busy || (!message && !minutes)}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button className="btn-primary text-xs" disabled={!canSend}>
|
||||
{busy ? 'Sending…' : 'Send broadcast'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user