First-run fixes from a clean install on Windows (#246)
Walking a new user's first minutes on a fresh Windows install: - A new account's rail entry and first conversations showed nowhere until a restart. An account's own group is listed on request, never announced, so a conversation arrived naming a group the window did not know. The window now asks for the rail again when that happens, when an unknown account appears, and after removing one (whose tile also stayed). - Kick's "Just watch" and adding an IRC network left the user on the accounts page looking at "select a server on the left" with nothing on the left. Both now go to the account's join page. - Joining from the join page stayed on the form; joinAndOpen opens the conversation once it arrives (or at once if already joined), for Kick handles in any form and IRC channels and nicks. - A signed-out Kick viewer was offered a message box. It now says it is watching signed out, with a Sign in button. - The welcome text named "nobilis", which a new user has never heard of. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: 2f5918ec6e...8bcba8bd23
@@ -8384,3 +8384,15 @@ video.lightbox-media {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* In place of the box, for an account that can only watch. */
|
||||
.composer-watching {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
}
|
||||
|
||||
.composer-watching .button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ export function AccountsPanel(): JSX.Element {
|
||||
{accounts.length === 0 && (
|
||||
<>
|
||||
<p className="muted">
|
||||
No accounts yet. Pick a service above to connect one — nobilis keeps the connection
|
||||
alive in the background, so it survives closing this window.
|
||||
No accounts yet. Pick a service above to connect one — moho stays connected in the
|
||||
background, even with this window closed.
|
||||
</p>
|
||||
{/* Before the first account rather than after: adding one is the
|
||||
first thing that fetches anything, and somebody who needs all
|
||||
@@ -463,8 +463,14 @@ function AccountRow({ account }: { account: Account }): JSX.Element {
|
||||
type="button"
|
||||
className="button danger"
|
||||
onClick={() => {
|
||||
call('removeAccount', { accountId: account.id })
|
||||
void store.refreshBuffers()
|
||||
// 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
|
||||
@@ -742,6 +748,10 @@ function IrcForm({ onDone }: { onDone: () => void }): JSX.Element {
|
||||
await store.refreshAccounts()
|
||||
store.clearPendingLink()
|
||||
onDone()
|
||||
// On to choosing a channel, as for Kick - a network with nothing to
|
||||
// join on connect is otherwise a server tab and the accounts page.
|
||||
// Not when channels were named here: those arrive by themselves.
|
||||
if (!autojoin.trim()) store.setActivePanel('join', `${nick}@${host}`)
|
||||
} catch (e) {
|
||||
store.toast('error', (e as Error).message)
|
||||
} finally {
|
||||
@@ -1117,8 +1127,18 @@ function KickForm({ onDone }: { onDone: () => void }): JSX.Element {
|
||||
const add = (token?: string): void => {
|
||||
setBusy(true)
|
||||
void window.moho
|
||||
.rpc('addKickAccount', token ? { token, useTor } : { useTor })
|
||||
.then(() => onDone())
|
||||
.rpc<{ accountId: string }>('addKickAccount', token ? { token, useTor } : { useTor })
|
||||
.then((r) => {
|
||||
onDone()
|
||||
// Straight to choosing a streamer. A new Kick account watches
|
||||
// nothing, so it has no conversations and no rail entry yet - and
|
||||
// left on the accounts page, a first-time user was looking at
|
||||
// "select a server on the left" with nothing on the left, the way
|
||||
// forward being a small + in the footer. Signing in may bring the
|
||||
// account's follows with it; this is still where the next one is
|
||||
// added.
|
||||
if (r?.accountId) store.setActivePanel('join', r.accountId)
|
||||
})
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
.finally(() => setBusy(false))
|
||||
}
|
||||
|
||||
@@ -548,6 +548,23 @@ export function Composer(): JSX.Element | null {
|
||||
if (inputRef.current) setText(composerText(inputRef.current))
|
||||
}
|
||||
|
||||
// A Kick account with no sign-in watches; it cannot talk. Offering the
|
||||
// box anyway meant typing a message the service was always going to refuse.
|
||||
if (account && account.service === 'kick' && !account.hasPassword) {
|
||||
return (
|
||||
<div className="composer">
|
||||
<div className="divider-h" />
|
||||
<div className="composer-watching small muted">
|
||||
<Icon name="visibility" size={16} />
|
||||
<span>Watching signed out. Sign in to Kick to talk here.</span>
|
||||
<button type="button" className="button" onClick={() => store.setActivePanel('accounts')}>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="composer">
|
||||
<div className="divider-h" />
|
||||
|
||||
@@ -122,22 +122,19 @@ function SubmitField({
|
||||
|
||||
function IrcJoin({ account }: { account: Account }): JSX.Element {
|
||||
const store = useStore()
|
||||
const call = (method: string, params: Record<string, unknown>): void => {
|
||||
void window.moho.rpc(method, params).catch((e: Error) => store.toast('error', e.message))
|
||||
}
|
||||
return (
|
||||
<div className="panel join-panel">
|
||||
<SubmitField
|
||||
label="Join a channel"
|
||||
placeholder="#channel"
|
||||
onSubmit={(name) => call('joinBuffer', { accountId: account.id, name })}
|
||||
onSubmit={(name) => void store.joinAndOpen(account.id, name)}
|
||||
/>
|
||||
<SubmitField
|
||||
label="Message someone"
|
||||
placeholder="nick"
|
||||
// IRC has no "open a DM" call - a query buffer only exists once
|
||||
// something is sent, so this joins a buffer named for the nick.
|
||||
onSubmit={(nick) => call('joinBuffer', { accountId: account.id, name: nick })}
|
||||
onSubmit={(nick) => void store.joinAndOpen(account.id, nick)}
|
||||
/>
|
||||
<IrcChannelBrowser account={account} />
|
||||
</div>
|
||||
@@ -259,7 +256,7 @@ interface IrcChannelListing {
|
||||
}
|
||||
|
||||
function join(accountId: string, name: string, store: ReturnType<typeof useStore>): void {
|
||||
void window.moho.rpc('joinBuffer', { accountId, name }).catch((e: Error) => store.toast('error', e.message))
|
||||
void store.joinAndOpen(accountId, name)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,11 +276,7 @@ function KickJoin({ account }: { account: Account }): JSX.Element {
|
||||
<SubmitField
|
||||
label="Watch a streamer's chat"
|
||||
placeholder="handle, or a kick.com link"
|
||||
onSubmit={(name) =>
|
||||
void window.moho
|
||||
.rpc('joinBuffer', { accountId: account.id, name })
|
||||
.catch((e: Error) => store.toast('error', e.message))
|
||||
}
|
||||
onSubmit={(name) => void store.joinAndOpen(account.id, name)}
|
||||
/>
|
||||
<p className="small muted">
|
||||
Kick chat is public, so this works signed out. Signing in adds talking, and your
|
||||
|
||||
@@ -1306,6 +1306,17 @@ export class ChatStore {
|
||||
.catch((e: Error) => this.toast('error', e.message))
|
||||
}
|
||||
|
||||
private groupsTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
/** The rail again, once a burst of conversations naming new entries settles. */
|
||||
refreshGroupsSoon(): void {
|
||||
if (this.groupsTimer) clearTimeout(this.groupsTimer)
|
||||
this.groupsTimer = setTimeout(() => {
|
||||
this.groupsTimer = null
|
||||
void this.refreshGroups()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
/**
|
||||
* The rail. Kept in nobilis's order (its own `position`, then name), which
|
||||
* for Discord is the order the user arranged their servers in.
|
||||
@@ -3524,6 +3535,18 @@ export class ChatStore {
|
||||
return
|
||||
}
|
||||
this.set({ buffers: [...buffers, { unread: 0, highlight: false, ...data }] })
|
||||
// Filed under a rail entry this window has never been told of. An
|
||||
// account's own entry is not announced when the account is made - the
|
||||
// daemon lists one for every account when asked - so the first
|
||||
// conversation of an account added during this run arrived pointing at
|
||||
// nothing, and showed nowhere until a restart: a new Kick viewer joined
|
||||
// a streamer and watched the window not change.
|
||||
if (data.groupId && !this.state.groups.some((g) => g.id === data.groupId)) this.refreshGroupsSoon()
|
||||
const waiting = this.openOnArrival
|
||||
if (waiting && waiting.accountId === data.accountId && data.kind !== 'server' && Date.now() < waiting.until) {
|
||||
this.openOnArrival = null
|
||||
void this.selectBuffer(data.id)
|
||||
}
|
||||
this.followPeekJoin(data)
|
||||
bestEffort(window.moho.rpc('subscribe', { bufferId: data.id }), `subscribe ${data.id}`)
|
||||
// A channel this client had not heard of may already hold mentions in the
|
||||
@@ -3578,6 +3601,9 @@ export class ChatStore {
|
||||
})
|
||||
} else {
|
||||
void this.refreshAccounts()
|
||||
// And its rail entry, which nothing else announces: a new account
|
||||
// showed nowhere on the left until its first conversation arrived.
|
||||
this.refreshGroupsSoon()
|
||||
}
|
||||
if (data.error) this.toast('error', `${data.accountId}: ${data.error}`)
|
||||
}
|
||||
@@ -4444,6 +4470,40 @@ export class ChatStore {
|
||||
}
|
||||
|
||||
/** The room that was being joined has turned up: open it for real. */
|
||||
/** A join asked for from the join page, waiting for its conversation. */
|
||||
private openOnArrival: { accountId: string; until: number } | null = null
|
||||
|
||||
/**
|
||||
* Joins, and opens what was joined once it exists.
|
||||
*
|
||||
* The join page used to ask and stay where it was, so somebody who had just
|
||||
* joined a channel - a first-time user, especially, with nothing else on
|
||||
* screen - went on looking at the form, and had to find the result on the
|
||||
* left for themselves. One already joined opens at once.
|
||||
*/
|
||||
async joinAndOpen(accountId: string, name: string): Promise<void> {
|
||||
// As the conversation will be named: an IRC channel as typed, a Kick
|
||||
// streamer out of whichever form the handle came in.
|
||||
const typed = name.trim().toLowerCase()
|
||||
const wanted = typed.startsWith('#')
|
||||
? typed
|
||||
: typed.replace(/^https?:\/\/(www\.)?kick\.com\//, '').replace(/^@/, '').replace(/[/?#].*$/, '')
|
||||
const existing = this.state.buffers.find(
|
||||
(b) => b.accountId === accountId && b.name.toLowerCase() === wanted && !isJoining(b)
|
||||
)
|
||||
if (existing) {
|
||||
void this.selectBuffer(existing.id)
|
||||
return
|
||||
}
|
||||
this.openOnArrival = { accountId, until: Date.now() + 20_000 }
|
||||
try {
|
||||
await window.moho.rpc('joinBuffer', { accountId, name })
|
||||
} catch (e) {
|
||||
this.openOnArrival = null
|
||||
this.toast('error', (e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
private followPeekJoin(data: { id: string; remoteId?: string }): void {
|
||||
const peek = this.state.peek
|
||||
if (!peek?.joining || !data.remoteId) return
|
||||
|
||||
Reference in New Issue
Block a user