Settings shows the protocols you actually have accounts on

A rail of pages that cannot apply to anything is how a settings pane stops
being readable. IRC, Sneedchat, Matrix and Kick now appear only while there
is an account on them.

General, Tor and About are always there. Tor stays even with no account
using it, because it is how somebody arranges to reach a network they cannot
otherwise get to - needing an account first would be the wrong way round.

Discord and Slack are gone rather than filtered. They sat here marked
"soon", above a page saying each "gets its own settings here once its
backend exists" - which stopped being true for Discord some thousands of
lines ago. It has a backend; it has no settings of its own yet, which is a
different sentence. An entry that exists only to say a thing that is not so
is worse than no entry, and this rule would have hidden them anyway.

Removing the last account of a protocol takes its page away while somebody
may be reading it, so the selection falls back to General rather than
leaving a rail with nothing selected, which looks broken.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-12 10:41:33 -04:00
co-authored by Claude Opus 5
parent 18d4dd17ff
commit a23c8bbda3
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import {
ChoiceSetting,
DirectorySetting,
@@ -79,16 +79,33 @@ function GlobalHighlightKeywords(): JSX.Element {
* categories swapping a content pane on the right, sized for a handful of
* protocols rather than dozens of categories (hence no search).
*/
const CATEGORIES = [
{ id: 'general', label: 'General', available: true },
{ id: 'irc', label: 'IRC', available: true },
{ id: 'sneedchat', label: 'Sneedchat', available: true },
{ id: 'tor', label: 'Tor', available: true },
{ id: 'matrix', label: 'Matrix', available: true },
{ id: 'kick', label: 'Kick', available: true },
{ id: 'about', label: 'About', available: true },
{ id: 'discord', label: 'Discord', available: false },
{ id: 'slack', label: 'Slack', available: false }
/**
* The rail, and what each entry needs in order to be worth showing.
*
* `service` names the protocol an entry is about; an entry with one appears
* only while there is an account on it. Somebody with no IRC account has no
* use for a page of IRC settings, and a rail of pages that cannot apply to
* anything is how a settings pane stops being readable.
*
* General, Tor and About carry no `service` and are always there. Tor stays
* even with no account using it: it is how somebody arranges to reach a
* network they cannot otherwise get to, so needing an account first would be
* the wrong way round.
*
* Discord and Slack used to sit here marked "soon", which stopped being true
* for Discord some thousands of lines ago - it has a backend, it just has no
* settings of its own yet. An entry that exists only to say a thing that is
* not so is worse than no entry, and this rule would have hidden them both
* regardless.
*/
const CATEGORIES: { id: string; label: string; service?: Account['service'] }[] = [
{ id: 'general', label: 'General' },
{ id: 'irc', label: 'IRC', service: 'irc' },
{ id: 'sneedchat', label: 'Sneedchat', service: 'sneedchat' },
{ id: 'tor', label: 'Tor' },
{ id: 'matrix', label: 'Matrix', service: 'matrix' },
{ id: 'kick', label: 'Kick', service: 'kick' },
{ id: 'about', label: 'About' }
]
/**
@@ -178,18 +195,31 @@ interface UploadHost {
export function SettingsPanel(): JSX.Element {
const [selected, setSelected] = useState('general')
const accounts = useChat((s) => s.accounts)
const shown = useMemo(() => {
const have = new Set(accounts.map((a) => a.service))
return CATEGORIES.filter((c) => !c.service || have.has(c.service))
}, [accounts])
// Removing the last account of a protocol takes its page away while
// somebody may be reading it. Falling back rather than showing an empty
// pane, because a rail with nothing selected looks broken.
useEffect(() => {
if (!shown.some((c) => c.id === selected)) setSelected('general')
}, [shown, selected])
return (
<div className="settings">
<div className="settings-rail">
{CATEGORIES.map((c) => (
{shown.map((c) => (
<button
key={c.id}
type="button"
className={`settings-rail-item${selected === c.id ? ' active' : ''}${c.available ? '' : ' soon'}`}
className={`settings-rail-item${selected === c.id ? ' active' : ''}`}
onClick={() => setSelected(c.id)}
>
{c.available ? c.label : `${c.label} (soon)`}
{c.label}
</button>
))}
</div>
@@ -209,23 +239,11 @@ export function SettingsPanel(): JSX.Element {
)}
{selected === 'kick' && <KickSettings />}
{selected === 'about' && <AboutSettings />}
{(selected === 'discord' || selected === 'slack') && (
<ComingSoon label={CATEGORIES.find((c) => c.id === selected)!.label} />
)}
</div>
</div>
)
}
function ComingSoon({ label }: { label: string }): JSX.Element {
return (
<SettingsSection
title={label}
description="Not connected yet - this gets its own settings here once its backend exists, the same way IRC's does."
/>
)
}
/**
* Which microphone and speakers voice uses.
*