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:
@@ -100,6 +100,7 @@ export default function Visibility() {
|
||||
const [clanRoster, setClanRoster] = useState('members')
|
||||
const [servers, setServers] = useState({})
|
||||
const [news, setNews] = useState({})
|
||||
const [delivery, setDelivery] = useState({})
|
||||
const [mapFleet, setMapFleet] = useState({})
|
||||
const [mapServers, setMapServers] = useState({})
|
||||
const [busy, setBusy] = useState(false)
|
||||
@@ -114,6 +115,7 @@ export default function Visibility() {
|
||||
setClanRoster((state.clans && state.clans.roster) || 'members')
|
||||
setServers(Object.fromEntries(state.presence.servers.map((s) => [s.id, s.override || INHERIT])))
|
||||
setNews(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, Boolean(s.on)])))
|
||||
setDelivery(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, s.delivery || 'chat'])))
|
||||
if (state.map) {
|
||||
setMapFleet({ ...state.map.fleet })
|
||||
setMapServers(Object.fromEntries(state.map.servers.map((s) => [s.id, mapOverridesToForm(s.overrides)])))
|
||||
@@ -136,10 +138,11 @@ export default function Visibility() {
|
||||
const dirtyClans = clanRoster !== clans.roster
|
||||
const newsRows = (data.news && data.news.servers) || []
|
||||
const dirtyNews = newsRows.filter((s) => Boolean(news[s.id]) !== Boolean(s.on))
|
||||
const dirtyDelivery = newsRows.filter((s) => (delivery[s.id] || 'chat') !== (s.delivery || 'chat'))
|
||||
const mapCard = data.map || null
|
||||
const mapChanges = mapCard ? mapDiff(mapCard, mapFleet, mapServers) : null
|
||||
const dirtyMap = Boolean(mapChanges)
|
||||
const dirty = dirtyFleet || dirtyServers.length > 0 || dirtyClans || dirtyNews.length > 0 || dirtyMap
|
||||
const dirty = dirtyFleet || dirtyServers.length > 0 || dirtyClans || dirtyNews.length > 0 || dirtyDelivery.length > 0 || dirtyMap
|
||||
|
||||
const effective = (id) => servers[id] || fleet
|
||||
const widened = fleet !== 'staff' || rows.some((s) => effective(s.id) !== 'staff')
|
||||
@@ -157,6 +160,7 @@ export default function Visibility() {
|
||||
body.servers = Object.fromEntries(dirtyServers.map((s) => [s.id, servers[s.id] || null]))
|
||||
}
|
||||
if (dirtyNews.length) body.news = Object.fromEntries(dirtyNews.map((s) => [s.id, Boolean(news[s.id])]))
|
||||
if (dirtyDelivery.length) body.newsDelivery = Object.fromEntries(dirtyDelivery.map((s) => [s.id, delivery[s.id] || 'chat']))
|
||||
if (dirtyMap) body.map = mapChanges
|
||||
load(await api.adminVisibility.save(body))
|
||||
setSaved(true)
|
||||
@@ -255,7 +259,8 @@ export default function Visibility() {
|
||||
<Card title="News in game chat" subtitle="a published news post, said in each server’s chat">
|
||||
<p className="sans dim" style={{ fontSize: '0.8rem', margin: '0 0 8px' }}>
|
||||
When a news post is published, its title is said in the chat of every server switched on
|
||||
here. A server that is down when a post is published is skipped rather than told late.
|
||||
here. A server that is down when a post is published is skipped rather than told late. A popup
|
||||
needs PopupNotifications on that server; one without it refuses the post and says why.
|
||||
</p>
|
||||
{newsRows.length === 0 && (
|
||||
<p className="sans dim" style={{ fontSize: '0.82rem', margin: 0 }}>No servers are configured yet.</p>
|
||||
@@ -285,6 +290,17 @@ export default function Visibility() {
|
||||
{!s.enabled && <span className="dim" style={{ fontSize: '0.74rem' }}> · disabled</span>}
|
||||
</span>
|
||||
<span className="dim" style={{ fontSize: '0.78rem' }}>{news[s.id] ? 'says news' : 'off'}</span>
|
||||
{/* D142: where the post goes on this server when it is switched on. */}
|
||||
<select
|
||||
value={delivery[s.id] || 'chat'}
|
||||
onChange={(e) => setDelivery((prev) => ({ ...prev, [s.id]: e.target.value }))}
|
||||
disabled={!news[s.id]}
|
||||
aria-label={`Where news goes on ${s.name}`}
|
||||
style={{ marginLeft: 'auto', background: 'var(--panel-flat, transparent)', color: 'var(--text)', border: '1px solid var(--line)', borderRadius: 'var(--radius-input, 6px)', padding: '3px 6px', fontSize: '0.8rem' }}
|
||||
>
|
||||
<option value="chat">in chat</option>
|
||||
<option value="popup">as a popup</option>
|
||||
</select>
|
||||
</label>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user