feat(rust): chat titles, BetterChat group styles, the voice and popups (phase 17)
PLAN.md §33, D134-D143. Protocol 12. - Chat titles (D135-D137): per-server rules (stat, top N, text, colour) that rank the current wipe, and a mode (first | all | up to N). Worked out once in model/titles and read three ways: pushed whole to the game by a new titleSync loop (on change, restart or wipe), and on every leaderboard row as `titles`. Admin: PUT /servers/:id/titles. - Group styles (D138, D139): a site group may carry all twelve BetterChat fields (rust_perm_group_chat). They ride perm.sync with `expect` from the pushed ledger, which gains a value column; a field changed in game is a `chat-field` drift row with the game's value, adopted into the style or put back. A withdrawn style is one `chat-group` retirement, never for `default`, cleared from the ledger only once BetterChat removed it. - The voice (D140): one fleet setting naming a styled group; news and rust.announce chat lines carry its format and the plugin says them with no sender. Admin: GET/PUT /voice. - Popups (D141, D142): rust.announce gains `delivery` (still version 1, from rust.options.delivery); each server gains news_delivery beside the news switch; `popup-unavailable` is not retried. - GET /servers/:id/integrations reads, live, which optional mods a server has loaded. README lists BetterChat and PopupNotifications as optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -27,6 +27,7 @@ import { useCallback, useState } from 'react'
|
||||
import { ErrorState, Loading, useAsync } from '../../core.js'
|
||||
import { ago } from '../../lib/format.js'
|
||||
import api from '../../api.js'
|
||||
import ChatStyleSection from './ChatStyle.jsx'
|
||||
|
||||
const FLEET = '*'
|
||||
|
||||
@@ -159,6 +160,28 @@ function ServerState({ row, onSync, busy }) {
|
||||
function DriftRow({ row, onAdopt, onRevoke, busy }) {
|
||||
const subject = row.username ? `${row.username} (${row.subject})` : row.subject
|
||||
|
||||
// Phase 17: a field of a group's chat style somebody changed in game. Adopt
|
||||
// takes the game's value into the style; Revoke puts the site's value back.
|
||||
if (row.kind === 'chat-field') {
|
||||
return (
|
||||
<Row>
|
||||
<span style={{ minWidth: 0, flex: 1 }}>
|
||||
<strong style={{ fontWeight: 500 }}>{row.object}</strong>{' '}
|
||||
<span className="dim" style={{ fontSize: '0.78rem' }}>
|
||||
of group {row.subject}’s chat style is <code>{row.detail === null ? '(empty)' : row.detail}</code> in game ·{' '}
|
||||
{row.serverId} · seen {ago(row.firstSeen)}
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => onAdopt(row)} disabled={busy}>
|
||||
Adopt
|
||||
</button>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => onRevoke(row)} disabled={busy}>
|
||||
Put ours back
|
||||
</button>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<span style={{ minWidth: 0, flex: 1 }}>
|
||||
@@ -199,7 +222,7 @@ function pendingSet(servers) {
|
||||
return pending
|
||||
}
|
||||
|
||||
function GroupCard({ group, catalogue, servers, pending, onChanged, setError }) {
|
||||
function GroupCard({ group, catalogue, servers, pending, chatFields, onChanged, setError }) {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [member, setMember] = useState('')
|
||||
const [permission, setPermission] = useState('')
|
||||
@@ -227,6 +250,29 @@ function GroupCard({ group, catalogue, servers, pending, onChanged, setError })
|
||||
}),
|
||||
)
|
||||
|
||||
// The style is saved with the group, like its permissions (D138). Answers
|
||||
// whether it saved, so the editor stays open on a refusal and shows why.
|
||||
const saveStyle = async (chat) => {
|
||||
setBusy(true)
|
||||
setError('')
|
||||
try {
|
||||
await api.adminPermissions.saveGroup(group.name, {
|
||||
title: group.title,
|
||||
rank: group.rank,
|
||||
scope: group.scope,
|
||||
permissions: group.permissions,
|
||||
chat,
|
||||
})
|
||||
await onChanged()
|
||||
return true
|
||||
} catch (err) {
|
||||
setError(err.message || 'That style did not save.')
|
||||
return false
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={group.title || group.name}
|
||||
@@ -358,6 +404,8 @@ function GroupCard({ group, catalogue, servers, pending, onChanged, setError })
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<ChatStyleSection group={group} fields={chatFields} servers={servers} busy={busy} onSave={saveStyle} />
|
||||
|
||||
{servers.length > 1 && group.scope !== FLEET && (
|
||||
<p className="sans dim" style={{ fontSize: '0.74rem', margin: '10px 0 0' }}>
|
||||
This group exists on {group.scope} only. The other servers never receive it.
|
||||
@@ -453,7 +501,8 @@ export default function Permissions() {
|
||||
<p className="sans dim" style={{ fontSize: '0.8rem', marginTop: 0 }}>
|
||||
Nothing here is undone automatically. <strong>Adopt</strong> records it as the site’s
|
||||
own, so it survives the next wipe; <strong>Revoke</strong> removes it from the game on
|
||||
the next sync.
|
||||
the next sync. A chat style field changed in game is adopted into the style — which then
|
||||
reaches every server the group does — or put back to the site’s value.
|
||||
</p>
|
||||
{data.drift.map((row) => (
|
||||
<DriftRow
|
||||
@@ -564,6 +613,7 @@ export default function Permissions() {
|
||||
catalogue={data.catalogue || []}
|
||||
servers={servers}
|
||||
pending={pendingSet(servers)}
|
||||
chatFields={data.chatFields || []}
|
||||
onChanged={reload}
|
||||
setError={setError}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user