From f0be37796bb8f0b33e65889c42936ee5056df342 Mon Sep 17 00:00:00 2001 From: Waters Church Date: Wed, 16 Sep 2026 19:58:33 -0400 Subject: [PATCH] Slack 1.2.1: a clearer Send tile, and a send target you can see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The line above the message box said "Sending as Apollo Beach → #ops" with a Lock button beside it. The box's own placeholder already reads "Message #ops…" and the title bar already has a Lock, so both went. Quick replies keeps its line: it has no placeholder, so that is the only place its target is named. The bigger thing behind it: until a transcript was clicked, a send went to the first channel in the admin list whether or not that channel was on the dashboard — so a board showing only #ops would quietly aim at #general. The target now falls to the first configured channel whose transcript is actually up, which with a single transcript is that one. Only when the page shows no transcript at all does it fall back to the first configured channel. Co-Authored-By: Claude Opus 5 --- modules/slack/client.js | 35 +++++++++++++++++++++++------------ modules/slack/module.json | 6 +++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/modules/slack/client.js b/modules/slack/client.js index 59637e3..d35b041 100644 --- a/modules/slack/client.js +++ b/modules/slack/client.js @@ -141,13 +141,17 @@ const sendTarget = (() => { /** * Where a send goes, given the configured channels in admin order: the * selection while it is still configured and its transcript is on the - * page; otherwise the first configured channel. + * page, else the first configured channel that IS on this page — with one + * transcript up, that one — and only if the page shows none, the first + * configured channel. A send must never quietly go to a channel the + * operator cannot see. */ resolve(configured) { const list = Array.isArray(configured) ? configured : []; if (!list.length) return ''; if (id && list.includes(id) && this.hasTranscript(id)) return id; - return list[0]; + const onPage = list.find((c) => this.hasTranscript(c)); + return onPage || list[0]; }, on(fn) { bus.addEventListener('change', fn); @@ -1100,7 +1104,7 @@ function createSendTile({ root, moduleApi, mode }) { if (stopped || !state) return; if (token) { if (composeEls) { - composeEls.line.textContent = sendingLine(); + if (composeEls.line) composeEls.line.textContent = sendingLine(); if (composeEls.input) composeEls.input.placeholder = placeholder(); } } else renderGate(); @@ -1177,14 +1181,21 @@ function createSendTile({ root, moduleApi, mode }) { let composeEls = null; function buildCompose() { compose.textContent = ''; - const head = el('div', 'sl-compose-head'); - const line = el('span', 'sl-sending', sendingLine()); - const lockLink = el('button', 'sl-lock-link', 'Lock'); - lockLink.type = 'button'; - lockLink.title = 'Lock now'; - lockLink.addEventListener('click', () => lock('Locked')); - head.append(line, lockLink); - compose.appendChild(head); + // Send message names the channel in the box's own placeholder and locks + // from the title bar, so a line above the field would only say both + // again. Quick replies has no placeholder — there it is the only place + // the target is named, so it stays. + let line = null; + if (mode !== 'send') { + const head = el('div', 'sl-compose-head'); + line = el('span', 'sl-sending', sendingLine()); + const lockLink = el('button', 'sl-lock-link', 'Lock'); + lockLink.type = 'button'; + lockLink.title = 'Lock now'; + lockLink.addEventListener('click', () => lock('Locked')); + head.append(line, lockLink); + compose.appendChild(head); + } const foot = el('div', 'sl-compose-foot'); const msg = el('div', 'sl-msg'); @@ -1462,7 +1473,7 @@ function createSendTile({ root, moduleApi, mode }) { if (!s.sending?.enabled || !s.sending?.pinSet) return void lock(''); if (!composeEls) return void showCompose(); // the page was unlocked before this tile appeared if (composeEls) { - composeEls.line.textContent = sendingLine(); + if (composeEls.line) composeEls.line.textContent = sendingLine(); if (composeEls.input) composeEls.input.placeholder = placeholder(); } } else renderGate(); diff --git a/modules/slack/module.json b/modules/slack/module.json index a0f75bb..8785160 100644 --- a/modules/slack/module.json +++ b/modules/slack/module.json @@ -1,7 +1,7 @@ { "id": "slack", "name": "Slack", - "version": "1.2.0", + "version": "1.2.1", "proddash": ">=1.4.0", "description": "Live transcripts of this location's Slack channels, plus PIN-protected sending as the location", "client": "client.js", @@ -21,7 +21,7 @@ "help": "Create a Slack app in the workspace (api.slack.com/apps → Create New App → From scratch) and, under OAuth & Permissions → User Token Scopes, add channels:history, channels:read, groups:history, groups:read, chat:write, users:read, reactions:write (emoji reactions) and emoji:read (the workspace's custom emoji). Sign in to Slack as this location's account — the one named for the campus — open the app's OAuth & Permissions page and press Install to Workspace (or Reinstall to Workspace), then copy the User OAuth Token (xoxp-…) it shows and paste it here: messages from this machine are then literally from the location. A bot token (xoxb-…) works too; it posts as the app, with \"Send as\" naming the location." }, "Channels": { - "help": "Every channel picked here becomes its own transcript tile in the + picker, titled with the channel's name. The Send message and Quick replies tiles post to whichever transcript was clicked last — its bottom edge lights up in the accent colour — and to the first channel here until one is clicked. Only channels the account has joined are listed; join one in Slack first. Each channel is read on its own schedule, all of them together kept inside Slack's rate limit." + "help": "Every channel picked here becomes its own transcript tile in the + picker, titled with the channel's name. The Send message and Quick replies tiles post to whichever transcript was clicked last — its bottom edge lights up in the accent colour — and until one is clicked, to the first channel here whose transcript is on that dashboard: with a single transcript up, that one. A send never goes to a channel the dashboard is not showing. Only channels the account has joined are listed; join one in Slack first. Each channel is read on its own schedule, all of them together kept inside Slack's rate limit." }, "Sending": { "toggle": "sendEnabled", @@ -49,7 +49,7 @@ "default": [], "group": "Channels", "optionsRoute": "/channels", - "help": "One transcript tile per channel; the first one is where sends go until a transcript is clicked. Only channels the account has joined are listed — join one in Slack first." + "help": "One transcript tile per channel; until a transcript is clicked, sends go to the first channel here that the dashboard is actually showing. Only channels the account has joined are listed — join one in Slack first." }, "pollSeconds": { "type": "number",