From 4d2480dba29de2004d9ee2e45b1805317fb85ca1 Mon Sep 17 00:00:00 2001 From: Salastil Date: Sat, 12 Sep 2026 10:45:27 -0400 Subject: [PATCH] The account picker asks the daemon which protocols there are The last copy of the protocol list. `ADDABLE` named five services and the daemon named seven with a flag on each, and nothing made them agree - the settings rail's copy went when that pane started following accounts instead, and this was the other one. The window still knows which services it can draw a sign-in form for, because that is the half only it can answer: a protocol the daemon speaks is not addable from a window with no form for it, and a chip that opens nothing would be worse than no chip. So what is offered is the intersection, in the daemon's order. A protocol that gains a backend shows up as soon as it gains a form; one the daemon stops offering disappears without this file being touched. Names come from the daemon too, which is why XMPP can be called XMPP while its id stays `jabber`. A daemon too old to answer leaves the window with the list it can draw, which is exactly what it had before it started asking. Two visible consequences, both checked against the running daemon: the picker's order changes, since the daemon lists Matrix second and this file listed it fourth; and jabber and slack stay hidden, because the daemon marks both unavailable. Closes #203. Co-Authored-By: Claude Opus 5 --- nobilis | 2 +- src/renderer/src/components/AccountsPanel.tsx | 57 ++++++++++++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/nobilis b/nobilis index 412c232..d584492 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit 412c23201d53eb3d70498495867ac6537c2dfe47 +Subproject commit d58449282294dbfccb3411e22664b0ffded240c6 diff --git a/src/renderer/src/components/AccountsPanel.tsx b/src/renderer/src/components/AccountsPanel.tsx index f107fd9..c1227a1 100644 --- a/src/renderer/src/components/AccountsPanel.tsx +++ b/src/renderer/src/components/AccountsPanel.tsx @@ -6,8 +6,28 @@ import { bufferDisplayName, classes, resolveMediaUrl, serviceIcon, serviceLabel import { IRC_NETWORKS, ircNetworkFor } from '../lib/networks' import type { Account } from '../../../shared/wire' -const ADDABLE = ['irc', 'discord', 'sneedchat', 'matrix', 'kick'] as const -type AddableService = (typeof ADDABLE)[number] +/** + * The services this window can draw a sign-in form for. + * + * Not a list of what moho supports - that is the daemon's to say, and it says + * it through `listProtocols`. This is the other half of the question, which + * only the window can answer: a protocol the daemon speaks but this has no + * form for cannot be added from here, and a chip that opens nothing would be + * worse than no chip. + * + * What is offered is the intersection, in the daemon's order. So a protocol + * that gains a backend appears here as soon as it gains a form, and one the + * daemon stops offering disappears without this file being touched. + */ +const FORMS = ['irc', 'discord', 'sneedchat', 'matrix', 'kick'] as const +type AddableService = (typeof FORMS)[number] + +/** What `listProtocols` answers with. */ +interface Protocol { + id: string + name: string + available: boolean +} /** * Account states that make a service's group open itself. @@ -50,12 +70,33 @@ export function AccountsPanel(): JSX.Element { */ const [openGroups, setOpenGroups] = useMapPref('accountGroupOpen') + // Which protocols the daemon offers, asked once. The two are deployed + // separately and either can be older than the other, which is the whole + // reason to ask rather than to assume. + const [protocols, setProtocols] = useState(null) + useEffect(() => { + void window.moho + .rpc('listProtocols') + // A daemon too old to answer leaves the window with what it can draw, + // which is what it did before it asked at all. + .catch(() => null) + .then(setProtocols) + }, []) + + const addable: AddableService[] = ( + protocols + ? protocols.filter((p) => p.available).map((p) => p.id) + : (FORMS as readonly string[]).slice() + ).filter((id): id is AddableService => (FORMS as readonly string[]).includes(id)) + + const nameFor = (service: string): string => + protocols?.find((p) => p.id === service)?.name ?? serviceLabel(service) + // Grouped by service, in the order the picker offers them, so the two // halves of this page agree about what order services come in. - const byService = ADDABLE.map((service) => ({ - service, - accounts: accounts.filter((a) => a.service === service) - })).filter((group) => group.accounts.length > 0) + const byService = addable + .map((service) => ({ service, accounts: accounts.filter((a) => a.service === service) })) + .filter((group) => group.accounts.length > 0) return (
@@ -66,7 +107,7 @@ export function AccountsPanel(): JSX.Element {

Add an account

- {ADDABLE.map((service) => { + {addable.map((service) => { const icon = serviceIcon(service) return ( ) })}