First-run fixes: remove confirmation, matrix.org sign-up, Kick header and footer, Matrix mark (#259, #260, #261, #262)

- #261: Remove account asks first, in place of the button, and says what
  goes - the saved sign-in, the history kept here, and for Matrix the
  device keys. "Keep it" puts the button back.
- #259: a homeserver that delegates its accounts (matrix.org) is closed to
  the registration API and open on its account service's site. The form
  said "not accepting new accounts" and greyed out Create account; it now
  says the server signs people up on its own website, and Create account
  opens it.
- #260: worded chat modes (Kick's "followers only") are set as text on one
  line rather than as mode letters in monospace, which wrapped into a
  two-line box over the channel's picture.
- #262: a signed-out Kick account's footer says "Watching, signed out"
  rather than "Online"; mute and deafen show on services with calls, or
  anywhere while a call is up, not beside IRC or Kick. Matrix's icon is its
  "[m]" mark rather than the wordmark, unreadable at icon size.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-03 23:03:26 -04:00
co-authored by Claude Opus 5.5
parent 76eabacc7c
commit 16fa8a2213
6 changed files with 118 additions and 23 deletions
+17
View File
@@ -5078,6 +5078,13 @@ video.lightbox-media {
border-radius: var(--corner-radius-small);
background: var(--surface-container-high);
color: var(--surface-variant-text);
/* One line, and not the thing that gives way: the stream title beside it
is what shrinks into an ellipsis. */
white-space: nowrap;
flex: none;
}
.channel-modes.letters {
font-family: var(--font-mono, monospace);
letter-spacing: 0.04em;
}
@@ -8396,3 +8403,13 @@ video.lightbox-media {
.composer-watching .button {
margin-left: auto;
}
/* Removing an account, asked in place of the button that asked for it. */
.remove-confirm {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
padding: var(--spacing-s) var(--spacing-m);
border: 1px solid #c9403c;
border-radius: var(--corner-radius-small);
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<!-- Matrix's short mark, "[m]": the wordmark is unreadable at icon size. -->
<path fill="#fff" d="M2 1.5h3.5v2H4v17h1.5v2H2zM22 1.5h-3.5v2H20v17h-1.5v2H22z"/>
<path fill="#fff" d="M6.5 8h1.9v1.05C9 8.3 9.8 7.85 10.8 7.85c1.05 0 1.8.45 2.2 1.25.6-.8 1.45-1.25 2.5-1.25 1.7 0 2.6 1.05 2.6 2.95V16h-2v-4.9c0-1.05-.4-1.55-1.2-1.55-.9 0-1.45.65-1.45 1.75V16h-2v-4.9c0-1.05-.4-1.55-1.2-1.55-.9 0-1.45.65-1.45 1.75V16h-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

+75 -20
View File
@@ -300,6 +300,7 @@ function AccountRow({ account }: { account: Account }): JSX.Element {
const media = useMediaUrl(account.id)
const store = useStore()
const [expanded, setExpanded] = useState(false)
const [confirmingRemove, setConfirmingRemove] = useState(false)
const icon = serviceIcon(account.service)
const connected = account.state === 'connected'
/**
@@ -459,22 +460,44 @@ function AccountRow({ account }: { account: Account }): JSX.Element {
on and never find again. */}
<IgnoreList account={account} />
<button
type="button"
className="button danger"
onClick={() => {
// Its conversations and its rail entry with it, once it is
// gone. An account's own entry is listed for as long as the
// account exists, and nothing announces it going, so a removed
// account's tile stayed until a restart.
void window.moho
.rpc('removeAccount', { accountId: account.id })
.then(() => Promise.all([store.refreshAccounts(), store.refreshBuffers(), store.refreshGroups()]))
.catch((e: Error) => store.toast('error', e.message))
}}
>
<Icon name="delete" size={16} /> Remove account
</button>
{/* Asked first, in place. Removing takes the saved sign-in, the
history kept here and - for Matrix - this device's keys, and
none of it comes back; it used to happen on the first click. */}
{confirmingRemove ? (
<div className="remove-confirm" role="alertdialog" aria-label="Remove this account?">
<span className="small">
Remove {account.displayName || account.id}? Its saved sign-in and the history kept
here are deleted
{account.service === 'matrix' ? ', and so are this device’s keys, so encrypted history stays unreadable if you add it again' : ''}
.
</span>
<div className="field-row">
<button
type="button"
className="button danger"
onClick={() => {
// Its conversations and its rail entry with it, once it
// is gone. An account's own entry is listed for as long
// as the account exists, and nothing announces it going,
// so a removed account's tile stayed until a restart.
void window.moho
.rpc('removeAccount', { accountId: account.id })
.then(() => Promise.all([store.refreshAccounts(), store.refreshBuffers(), store.refreshGroups()]))
.catch((e: Error) => store.toast('error', e.message))
}}
>
<Icon name="delete" size={16} /> Remove
</button>
<button type="button" className="button" autoFocus onClick={() => setConfirmingRemove(false)}>
Keep it
</button>
</div>
</div>
) : (
<button type="button" className="button danger" onClick={() => setConfirmingRemove(true)}>
<Icon name="delete" size={16} /> Remove account
</button>
)}
</div>
)}
</div>
@@ -1504,6 +1527,12 @@ function MatrixForm(): JSX.Element {
const [flows, setFlows] = useState<string[] | null>(null)
/** Whether this homeserver's accounts live with an OAuth provider. */
const [delegated, setDelegated] = useState(false)
/**
* Where a delegating server signs people up: its account service's own
* site. matrix.org is one - closed to registration through the API this
* form uses, open to anyone through account.matrix.org.
*/
const [signUpSite, setSignUpSite] = useState('')
/** Whether the homeserver keeps QR sign-in mailboxes (MSC4108). */
const [qrOffered, setQrOffered] = useState(false)
/** A QR sign-in under way: the code to show, and whether it now wants digits. */
@@ -1547,12 +1576,16 @@ function MatrixForm(): JSX.Element {
// say what the homeserver itself accepts, and this says whether it has
// handed its accounts to somebody else entirely.
void window.moho
.rpc<{ delegated: boolean; qr?: boolean }>('matrixAuthMetadata', { useTor, homeserverUrl: url })
.rpc<{ delegated: boolean; qr?: boolean; issuer?: string }>('matrixAuthMetadata', { useTor, homeserverUrl: url })
.then((a) => {
setDelegated(!!a.delegated)
setQrOffered(!!a.qr)
setSignUpSite(a.delegated && a.issuer && /^https:\/\//.test(a.issuer) ? a.issuer : '')
})
.catch(() => {
setDelegated(false)
setSignUpSite('')
})
.catch(() => setDelegated(false))
void window.moho
.rpc<{ open: boolean; needsToken: boolean; wants: string[] }>('matrixRegistrationFlows', { useTor,
homeserverUrl: url
@@ -1667,8 +1700,19 @@ function MatrixForm(): JSX.Element {
/>
</label>
)}
{registration?.open === false && (
<p className="small muted">This homeserver is not accepting new accounts.</p>
{/* A server that hands its accounts to an account service is closed to
the registration API and open on that service's site - matrix.org
is. Said as "not accepting new accounts", a new user's first look at
Matrix told them sign-up was impossible. */}
{signUpSite ? (
<p className="small muted">
This server signs people up on its own website, {new URL(signUpSite).host}. Make the
account there, then come back and use Sign in with a code.
</p>
) : (
registration?.open === false && (
<p className="small muted">This homeserver is not accepting new accounts.</p>
)
)}
{/* While a code sign-in is waiting. The code is the whole of what
somebody needs, so it is the largest thing here and selectable -
@@ -1827,6 +1871,16 @@ function MatrixForm(): JSX.Element {
server wants a captcha, an email or its terms agreed to, the
daemon says which and says to sign up on the server's own page,
because those need a person somewhere this window is not. */}
{signUpSite ? (
<button
type="button"
className="button subtle"
title={`Opens ${signUpSite} in your browser`}
onClick={() => void window.moho.openExternal(signUpSite)}
>
Create account <Icon name="open_in_new" size={14} />
</button>
) : (
<button
type="button"
className="button subtle"
@@ -1858,6 +1912,7 @@ function MatrixForm(): JSX.Element {
>
Create account
</button>
)}
</div>
{status && <p className="small muted">{status}</p>}
<p className="small muted">
@@ -835,7 +835,13 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
with the long form, since the letters are only obvious to people who
already knew. */}
{buffer.channelModes && (
<span className="channel-modes small" title={describeModes(buffer.channelModes)}>
<span
// Mode letters ("+nt") are set like code; a service that names its
// modes in words - Kick's "followers only" - is set like text, or it
// wraps into a two-line box in a monospace fallback.
className={`channel-modes small${/^[+-][A-Za-z]/.test(buffer.channelModes) ? ' letters' : ''}`}
title={describeModes(buffer.channelModes)}
>
{buffer.channelModes}
</span>
)}
+13 -1
View File
@@ -55,6 +55,7 @@ export function UserFooter({ account }: { account?: Account }): JSX.Element {
const store = useStore()
const { menu, open, close } = useContextMenu()
const voice = useChat((s) => s.voicePrefs)
const inACall = useChat((s) => s.voiceSessions.length > 0 || !!s.activeCall)
const details = useChat((s) => s.connectionDetail)
// With no account selected there is nobody to show, but the footer still
@@ -64,6 +65,13 @@ export function UserFooter({ account }: { account?: Account }): JSX.Element {
const status = effectiveStatus(account)
const name = account.displayName || account.id
const detail = details[account.id]
// A signed-out Kick account is a reader: it has no presence to show, and
// "Online" claimed one.
const watching = account.service === 'kick' && !account.hasPassword && status !== 'connecting' && status !== 'offline'
// Mute and deafen are the whole client's, for whatever call is up - so
// shown on a service that has calls, or anywhere while one is running,
// and not beside an IRC or Kick account with nothing to apply them to.
const showVoice = account.service === 'discord' || account.service === 'matrix' || inACall
return (
<>
@@ -77,10 +85,12 @@ export function UserFooter({ account }: { account?: Account }): JSX.Element {
<Avatar name={name} url={account.avatarUrl} size={28} status={status} accountId={account.id} />
<span className="user-identity">
<span className="ellipsis user-name">{name}</span>
<span className="ellipsis small muted">{label(status)}</span>
<span className="ellipsis small muted">{watching ? 'Watching, signed out' : label(status)}</span>
</span>
</button>
{showVoice && (
<>
<button
type="button"
className={`user-audio-button${voice.micMuted ? ' muted' : ''}`}
@@ -102,6 +112,8 @@ export function UserFooter({ account }: { account?: Account }): JSX.Element {
>
<Icon name={voice.deafened ? 'headset_off' : 'headset_mic'} size={18} />
</button>
</>
)}
{/* Joining something new, beside the account it would be joined on.
It used to sit in the heading above the channel list, where it read
+1 -1
View File
@@ -1,6 +1,6 @@
import discordSvg from '../assets/discord.svg'
import sneedchatMark from '../assets/sneedchat.png'
import matrixSvg from '../assets/matrix.svg'
import matrixSvg from '../assets/matrix-mark.svg'
import kickSvg from '../assets/kick.svg'
/**