Skip to content

Commit 6e997b4

Browse files
authored
Allow permanent moderators to delegate temporary access (#420)
* Add temporary moderator capability foundation * Add temporary moderator socket actions * Clarify temporary moderator revocation * Add temporary moderator controls * Document and verify temporary moderators * Fold temporary moderation into friend control * Allow revoking disconnected temporary moderators * Simplify temporary moderator architecture * Separate moderation and clear capabilities * Reuse choice dialog for moderator grants * Bound temporary moderator grants globally * Streamline temporary moderator actions * Simplify live moderator capability refresh * Consolidate temporary moderator coverage * Keep temporary moderator docs concise * Use authoritative temporary moderator updates * Keep grant validation at socket boundary * Unify temporary moderator grant mutations * Compact temporary moderator state * Trim temporary moderator choice flow * Remove redundant capability scenario * Unify moderation action dialogs
1 parent 1c7631e commit 6e997b4

31 files changed

Lines changed: 869 additions & 409 deletions

AGENTS.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ secret/IP ban expires. The browser schedules one reconnect at that boundary so
249249
non-moderator report targeting the reporter's own socket or another socket with
250250
the same non-empty, secret-derived user identity.
251251

252+
Board state and presence expose `canBan` separately from `canClear`. Moderation
253+
UI and moderator markers use `canBan`; Clear-tool access, large-batch admission,
254+
and destructive rate-limit bypasses use `canClear`.
255+
252256
Before the reported socket is closed, the server emits
253257
`moderation_disconnect { "banDurationMs": <duration>, "source": "moderator" | "peer_report", "moderationRule"?: "<rule>" }`.
254258
Moderator actions use `source: "moderator"`; `0` means a warning and a positive
@@ -262,7 +266,13 @@ server emits `user_reported` only to connected moderators on that board. The
262266
`{ "reporterName": "<display name>", "reportedName": "<display name>" }`.
263267
Moderator warning/ban actions do not emit `user_reported`; warning actions only
264268
disconnect the reported user, while ban actions also ban the reported secret and
265-
IP.
269+
IP. Active moderators are protected targets based on authoritative live
270+
capabilities, including when the reporter is a temporary moderator.
271+
272+
Permanent moderators use `set_temporary_moderator { socketId, durationMs }` to
273+
grant up to one week or revoke with `0`. Grants are board-scoped, process-local,
274+
secret-keyed across tabs, lost on restart, and cannot be delegated by temporary
275+
moderators. Changes refresh board state and presence for every matching socket.
266276

267277
Client write messages normally have top-level `tool` and `type` fields.
268278
Tool-owned batches have top-level `tool` plus `_children`; each child carries its

client-data/board.css

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -235,45 +235,58 @@ html[data-ui-direction="rtl"]
235235
text-transform: uppercase;
236236
}
237237

238-
.moderation-action-durations {
239-
display: flex;
240-
flex-wrap: wrap;
238+
.moderation-action-stacked {
239+
display: grid;
241240
gap: 6px;
241+
margin-bottom: 14px;
242+
}
243+
244+
.moderation-action-stacked .moderation-action-choice {
245+
width: 100%;
246+
}
247+
248+
.moderation-action-segmented {
249+
display: flex;
250+
width: 100%;
242251
margin-bottom: 12px;
243252
}
244253

245-
.moderation-action-duration {
246-
flex: 0 0 auto;
254+
.moderation-action-segmented .moderation-action-choice {
255+
flex: 1 1 0;
256+
min-width: 0;
257+
margin-inline-start: -1px;
258+
border-radius: 0;
247259
border-color: var(--board-control-border-strong);
248260
background: #ffffff;
249261
color: #111827;
250262
}
251263

252-
.moderation-action-duration.wbo-dialog-button-warning,
253-
.moderation-action-duration.wbo-dialog-button-danger {
264+
.moderation-action-segmented .moderation-action-choice:first-child {
265+
margin-inline-start: 0;
266+
border-start-start-radius: var(--board-control-radius);
267+
border-end-start-radius: var(--board-control-radius);
268+
}
269+
270+
.moderation-action-segmented .moderation-action-choice:last-child {
271+
border-start-end-radius: var(--board-control-radius);
272+
border-end-end-radius: var(--board-control-radius);
273+
}
274+
275+
.moderation-action-segmented .wbo-dialog-button-warning,
276+
.moderation-action-segmented .wbo-dialog-button-danger {
254277
border-color: var(--board-control-border-strong);
255278
background: #ffffff;
256279
color: #111827;
257280
}
258281

259-
.moderation-action-duration-selected {
282+
.moderation-action-choice-selected {
283+
z-index: 1;
260284
border-color: var(--board-accent-strong);
261285
background: var(--board-control-bg-active);
262286
box-shadow: 0 0 0 1px var(--board-accent-strong);
263287
}
264288

265-
.moderation-action-status {
266-
position: absolute;
267-
width: 1px;
268-
height: 1px;
269-
padding: 0;
270-
margin: -1px;
271-
overflow: hidden;
272-
clip: rect(0 0 0 0);
273-
border: 0;
274-
}
275-
276-
.moderation-action-rules {
289+
.moderation-action-grid {
277290
display: grid;
278291
grid-template-columns: repeat(2, 1fr);
279292
gap: 8px;
@@ -340,7 +353,7 @@ html[data-ui-direction="rtl"]
340353
}
341354

342355
@media (max-width: 380px) {
343-
.moderation-action-rules {
356+
.moderation-action-grid {
344357
grid-template-columns: 1fr;
345358
}
346359
}
@@ -1134,6 +1147,10 @@ select {
11341147
width: max-content;
11351148
}
11361149

1150+
#tools > .tool[hidden] {
1151+
display: none;
1152+
}
1153+
11371154
#menu .tool:hover {
11381155
max-width: 100%;
11391156
}

client-data/board.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@
6262
<div id="menuItems">
6363
<ul id="tools" class="tools">
6464
{{#tools}}
65-
<li class="tool disabledTool" tabindex="-1" id="toolID-{{id}}" data-tool-id="{{id}}" aria-label="{{label}}"
65+
<li class="tool disabledTool" tabindex="-1" id="toolID-{{id}}" data-tool-id="{{id}}" aria-label="{{label}}" {{^visible}}hidden{{/visible}}
6666
aria-disabled="true">
67-
<img class="tool-icon" width="35" height="35" src="{{iconUrl}}" alt="" aria-hidden="true"
67+
<img class="tool-icon" width="35" height="35" src="{{initialIconUrl}}" data-icon-url="{{iconUrl}}" alt="" aria-hidden="true"
6868
fetchpriority="low" />
6969
<span class="tool-name">{{label}}</span>
7070
<img class="tool-icon secondaryIcon" width="35" height="35" src="data:," alt="" aria-hidden="true" />

client-data/js/board_access_module.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export class AccessModule {
2121
return this.boardState.canClear;
2222
}
2323

24+
get canBan() {
25+
return this.boardState.canBan === true;
26+
}
27+
28+
get canGrantTemporaryModerator() {
29+
return this.boardState.canGrantTemporaryModerator === true;
30+
}
31+
2432
get canReport() {
2533
return this.boardState.canReport !== false;
2634
}
@@ -34,27 +42,18 @@ export class AccessModule {
3442
const Tools = this.getTools();
3543
this.boardState = boardState;
3644
Tools.connection.scheduleAccessRefresh(boardState.accessRefreshAfterMs);
37-
3845
// Hide editing affordances whenever the user cannot edit (a read-only board,
3946
// or a banned user on a writable one). The drawing tools themselves are
4047
// gated by shouldDisplayTool, which is capability-aware.
4148
const hideEditingTools = !this.canEdit;
4249
const settings = document.getElementById("settings");
4350
if (settings) settings.style.display = hideEditingTools ? "none" : "";
4451

45-
Object.keys(Tools.toolRegistry.mounted || {}).forEach((toolName) => {
46-
const toolElem = document.getElementById(`toolID-${toolName}`);
47-
if (!toolElem) return;
48-
toolElem.style.display = Tools.toolRegistry.shouldDisplayTool(toolName)
49-
? ""
50-
: "none";
51-
});
52-
52+
Tools.toolRegistry.syncRenderedToolAvailability();
5353
Tools.toolRegistry.syncDrawToolAvailability(true);
5454
Tools.presence.schedulePresenceRender();
5555

5656
if (
57-
hideEditingTools &&
5857
Tools.toolRegistry.current &&
5958
!Tools.toolRegistry.shouldDisplayTool(Tools.toolRegistry.current.name) &&
6059
Tools.toolRegistry.mounted.hand

client-data/js/board_page_state.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const DEFAULT_BOARD_STATE = /** @type {BoardState} */ (
88
readonly: false,
99
canEdit: true,
1010
canClear: false,
11+
canBan: false,
12+
canGrantTemporaryModerator: false,
1113
canReport: true,
1214
canWrite: true,
1315
})
@@ -55,7 +57,7 @@ export function normalizeBoardState(value) {
5557
return DEFAULT_BOARD_STATE;
5658
}
5759
const state =
58-
/** @type {{readonly?: boolean, canEdit?: boolean, canClear?: boolean, canReport?: boolean, canWrite?: boolean, accessRefreshAfterMs?: number}} */ (
60+
/** @type {{readonly?: boolean, canEdit?: boolean, canClear?: boolean, canBan?: boolean, canGrantTemporaryModerator?: boolean, canReport?: boolean, canWrite?: boolean, accessRefreshAfterMs?: number}} */ (
5961
value
6062
);
6163
const canEdit = state.canEdit === true || state.canWrite === true;
@@ -69,6 +71,8 @@ export function normalizeBoardState(value) {
6971
readonly: state.readonly === true,
7072
canEdit,
7173
canClear: state.canClear === true,
74+
canBan: state.canBan === true,
75+
canGrantTemporaryModerator: state.canGrantTemporaryModerator === true,
7276
canReport: state.canReport !== false,
7377
canWrite: canEdit,
7478
...(accessRefreshAfterMs === undefined ? {} : { accessRefreshAfterMs }),

0 commit comments

Comments
 (0)