Offer the IRC networks people actually join

Issues #12 and #13. Typing a hostname and port correctly is the one
step of adding an IRC account where a small mistake looks like the
server being down, so the common networks are offered ready-made and
fill the fields - which stay editable, and a network not on the list is
typed in exactly as before.

The same table gives each of those networks its own colour and letters
in the rail, instead of the identical "#" every IRC account shared -
telling one entry from another being the thing that column exists for.
Initials rather than logos, deliberately: those belong to the networks
that own them, and a wrong-looking copy of somebody's logo is worse
than two clean letters.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-28 01:11:56 -04:00
co-authored by Claude Opus 5
parent bfd892ade1
commit f6ee2e3d7e
3 changed files with 93 additions and 0 deletions
@@ -3,6 +3,7 @@ import { Icon, IconButton, MaskIcon } from './Icon'
import { MatrixAccountTools } from './MatrixAccountTools'
import { useChat, usePref, useStore } from '../state/hooks'
import { bufferDisplayName, resolveMediaUrl, serviceIcon, serviceLabel } from '../lib/util'
import { IRC_NETWORKS, ircNetworkFor } from '../lib/networks'
import type { Account } from '../../../shared/wire'
const ADDABLE = ['irc', 'discord', 'sockchat', 'matrix'] as const
@@ -380,6 +381,32 @@ function IrcForm({ onDone }: { onDone: () => void }): JSX.Element {
<span className="small muted">Nickname</span>
<input className="text-field" value={nick} onChange={(e) => setNick(e.target.value)} />
</label>
{/* Typing a hostname and port correctly is the one step here where a
small mistake looks like the server being down, so the networks
people actually join are offered ready-made. Choosing one fills
the fields rather than hiding them: they stay editable, and a
network not on the list is typed in exactly as before. */}
<label className="field">
<span className="small muted">Network</span>
<select
className="text-field"
value={ircNetworkFor(host)?.id ?? ''}
onChange={(e) => {
const n = IRC_NETWORKS.find((x) => x.id === e.target.value)
if (!n) return
setHost(n.host)
setPort(String(n.port))
setSsl(n.tls)
}}
>
<option value="">Other…</option>
{IRC_NETWORKS.map((n) => (
<option key={n.id} value={n.id}>
{n.name}
</option>
))}
</select>
</label>
<label className="field">
<span className="small muted">Server</span>
<input
@@ -3,6 +3,7 @@ import { ContextMenu, useContextMenu } from './ContextMenu'
import { Icon, IconButton, MaskIcon } from './Icon'
import { Avatar } from './Avatar'
import { LeaveConfirm } from './LeaveConfirm'
import { ircNetworkFor } from '../lib/networks'
import { useChat, usePref, useStore } from '../state/hooks'
import { bufferDisplayName, classes, nickColor, resolveMediaUrl, serviceIcon } from '../lib/util'
import {
@@ -92,6 +93,19 @@ function GroupFace({ group, customIcon }: { group: RailGroup; customIcon?: strin
return <img className="rail-mark" src={service.mark} alt="" />
}
if (group.kind === 'account') {
// A known IRC network gets its own colour and letters rather than the
// same generic "#" every network would otherwise share - which is the
// one thing the rail exists to avoid. Initials rather than logos: those
// belong to the networks that own them, and a wrong-looking copy of
// somebody's logo is worse than two clean letters.
const network = group.service === 'irc' ? ircNetworkFor(group.name) : null
if (network) {
return (
<span className="rail-initials" style={{ color: network.colour }}>
{network.mark}
</span>
)
}
return service.mark ? <MaskIcon src={service.mark} size={22} /> : <Icon name={service.glyph!} size={22} />
}
return (
+52
View File
@@ -0,0 +1,52 @@
/**
* The IRC networks somebody is actually likely to be joining.
*
* Typing a hostname and a port correctly is the one step of adding an IRC
* account where a small mistake looks like the server being down, so the
* common ones are offered ready-made. Deliberately short: this is a shortcut
* for the networks people use, not a directory - "Other" is always there and
* is what every network not on the list uses.
*
* Colours are the network's own where it has a recognisable one, and are used
* to tell one server tile from another in the rail. Marks are initials rather
* than logos: those belong to the networks that own them, and a wrong-looking
* copy of somebody's logo is worse than two clean letters.
*/
export interface IrcNetwork {
id: string
name: string
host: string
port: number
tls: boolean
/** Two characters at most, for the rail tile. */
mark: string
colour: string
}
export const IRC_NETWORKS: IrcNetwork[] = [
{ id: 'libera', name: 'Libera.Chat', host: 'irc.libera.chat', port: 6697, tls: true, mark: 'Li', colour: '#7f4bd8' },
{ id: 'oftc', name: 'OFTC', host: 'irc.oftc.net', port: 6697, tls: true, mark: 'OF', colour: '#2f7fbf' },
{ id: 'rizon', name: 'Rizon', host: 'irc.rizon.net', port: 6697, tls: true, mark: 'Ri', colour: '#c94f4f' },
{ id: 'quakenet', name: 'QuakeNet', host: 'irc.quakenet.org', port: 6667, tls: false, mark: 'QN', colour: '#d08a2a' },
{ id: 'efnet', name: 'EFnet', host: 'irc.efnet.org', port: 6697, tls: true, mark: 'EF', colour: '#4c8f6b' },
{ id: 'undernet', name: 'Undernet', host: 'irc.undernet.org', port: 6697, tls: true, mark: 'UN', colour: '#3f6fa8' },
{ id: 'snoonet', name: 'Snoonet', host: 'irc.snoonet.org', port: 6697, tls: true, mark: 'Sn', colour: '#e06a3a' },
{ id: 'hackint', name: 'hackint', host: 'irc.hackint.org', port: 6697, tls: true, mark: 'hi', colour: '#5f9ea0' }
]
/**
* The network a hostname belongs to, or null.
*
* Matched on the registrable part of the host rather than the whole thing,
* since a network answers on many names - irc.libera.chat, but also
* ipv6.libera.chat and any number of round-robin aliases.
*/
export function ircNetworkFor(host: string): IrcNetwork | null {
const h = host.toLowerCase().replace(/:\d+$/, '')
return (
IRC_NETWORKS.find((n) => {
const domain = n.host.split('.').slice(-2).join('.')
return h === n.host || h.endsWith(`.${domain}`) || h === domain
}) ?? null
)
}