Ask the site which Sneedchat rooms exist

The room list was six entries in this client's source, so #lolcows - a
room the site has had all along - could not be joined without editing and
rebuilding. The daemon reads the real list off the chat page now, and the
account's settings ask for it when they are opened, filling in when the
answer arrives a few seconds later.

The built-in list is the fallback rather than the source. The site is
behind a proof-of-work gate over Tor and the read can fail; rooms
somebody can still tick beat an empty list, and when it does fail the
pane says so rather than quietly showing a short list as if it were
complete. #lolcows is in that fallback now too.
This commit is contained in:
2026-09-02 23:15:54 -04:00
parent d3332a47b2
commit 4fd2e0efe0
3 changed files with 65 additions and 9 deletions
+1 -1
Submodule nobilis updated: 3acf5be366...ce6bc94315
+55 -3
View File
@@ -513,19 +513,64 @@ function SneedchatRooms({
call: (method: string, params: Record<string, unknown>) => void
}): JSX.Element {
const enabled = account.sockchatRooms ?? []
/**
* The catalogue, from the site.
*
* It used to be six rooms written into this client, which is how #lolcows
* came to be unreachable without editing the source. The daemon reads the
* room switcher off the chat page - the same list a person sees down the
* side of the site - and this asks for it when the account's settings are
* opened.
*
* The built-in list is the fallback rather than the source: the site is
* behind a proof-of-work gate over Tor and the fetch can fail, and six
* rooms somebody can still tick is a better answer than none.
*/
const [rooms, setRooms] = useState(KNOWN_SOCKCHAT_ROOMS)
const [asking, setAsking] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
let live = true
setAsking(true)
// The answer to this is whatever the daemon read last, which may be
// nothing on a fresh start; the live read happens behind it and arrives
// as an event, because it takes about fifteen seconds through Tor and the
// site's gate and the daemon answers one request at a time.
void window.moho
.rpc<{ rooms: { id: number; name: string }[] }>('listSockChatRooms', { accountId: account.id })
.then((r) => live && r.rooms?.length && setRooms(r.rooms))
.catch((e: Error) => live && setError(e.message))
const stop = window.moho.onEvent((frame) => {
if (frame.event !== 'sockchatRooms') return
const data = frame.data as { accountId: string; rooms: { id: number; name: string }[] }
if (!live || data.accountId !== account.id || !data.rooms.length) return
setRooms(data.rooms)
setAsking(false)
setError('')
})
return () => {
live = false
stop()
}
}, [account.id])
const toggle = (id: number, on: boolean): void => {
const next = on
? enabled.some((r) => r.id === id)
? enabled
: [...enabled, KNOWN_SOCKCHAT_ROOMS.find((r) => r.id === id)!]
: [...enabled, rooms.find((r) => r.id === id)!]
: enabled.filter((r) => r.id !== id)
call('setSockChatRooms', { accountId: account.id, rooms: next })
}
return (
<div className="sasl-block">
<span className="small muted">Channels</span>
{KNOWN_SOCKCHAT_ROOMS.map((room) => (
<span className="small muted">
Channels{asking ? ' — asking the site…' : ''}
</span>
{rooms.map((room) => (
<label key={room.id} className="checkbox-row">
<input
type="checkbox"
@@ -535,6 +580,13 @@ function SneedchatRooms({
<span>#{room.name}</span>
</label>
))}
{/* Said rather than hidden: this list is the one this client shipped
with, and the site may well have rooms that are not in it. */}
{error && (
<span className="small muted">
Could not read the site&apos;s room list ({error}) — showing the rooms this client knows.
</span>
)}
{/* An account with none ticked still connects to #general - the daemon
falls back to it so a freshly added account is usable before anybody
has been here. Worth saying, or an empty list reads as "connected to
+9 -5
View File
@@ -1,9 +1,12 @@
/**
* The Sneedchat rooms an account can be connected to.
* The Sneedchat rooms this client knows about without asking.
*
* No server-side "list every room" endpoint exists, so this is the fixed
* catalogue the account was originally configured against. Adding a room the
* site actually has just means it can be toggled on too.
* No longer the catalogue: the daemon reads the real one off the site's own
* chat page (see listSockChatRooms), which is how #lolcows was reachable to
* everybody using the site and to nobody using this. This is what shows while
* that is being fetched, and what remains if it cannot be - the site sits
* behind a proof-of-work gate over Tor, and rooms somebody can still tick beat
* an empty list.
*
* Shared rather than owned by whichever pane happens to draw it: the rooms are
* a property of an account, and two places disagreeing about which rooms exist
@@ -15,5 +18,6 @@ export const KNOWN_SOCKCHAT_ROOMS: { id: number; name: string }[] = [
{ id: 15, name: 'keno-kasino' },
{ id: 16, name: 'fishtank' },
{ id: 18, name: 'beauty-parlor' },
{ id: 19, name: 'sports' }
{ id: 19, name: 'sports' },
{ id: 20, name: 'lolcows' }
]