diff --git a/nobilis b/nobilis index 2f5918e..8bcba8b 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit 2f5918ec6e270330a19848cfe06d8c915b300c10 +Subproject commit 8bcba8bd239f8f3fffe48ae9441f151a196c1e30 diff --git a/src/renderer/src/app.css b/src/renderer/src/app.css index 02a2050..c7d0f9d 100644 --- a/src/renderer/src/app.css +++ b/src/renderer/src/app.css @@ -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; +} diff --git a/src/renderer/src/components/AccountsPanel.tsx b/src/renderer/src/components/AccountsPanel.tsx index ed95e90..e871899 100644 --- a/src/renderer/src/components/AccountsPanel.tsx +++ b/src/renderer/src/components/AccountsPanel.tsx @@ -137,8 +137,8 @@ export function AccountsPanel(): JSX.Element { {accounts.length === 0 && ( <>

- 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.

{/* 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)) }} > 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)) } diff --git a/src/renderer/src/components/Composer.tsx b/src/renderer/src/components/Composer.tsx index 6a69748..ee63c66 100644 --- a/src/renderer/src/components/Composer.tsx +++ b/src/renderer/src/components/Composer.tsx @@ -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 ( +
+
+
+ + Watching signed out. Sign in to Kick to talk here. + +
+
+ ) + } + return (
diff --git a/src/renderer/src/components/JoinPanel.tsx b/src/renderer/src/components/JoinPanel.tsx index cebf523..ff9a764 100644 --- a/src/renderer/src/components/JoinPanel.tsx +++ b/src/renderer/src/components/JoinPanel.tsx @@ -122,22 +122,19 @@ function SubmitField({ function IrcJoin({ account }: { account: Account }): JSX.Element { const store = useStore() - const call = (method: string, params: Record): void => { - void window.moho.rpc(method, params).catch((e: Error) => store.toast('error', e.message)) - } return (
call('joinBuffer', { accountId: account.id, name })} + onSubmit={(name) => void store.joinAndOpen(account.id, name)} /> call('joinBuffer', { accountId: account.id, name: nick })} + onSubmit={(nick) => void store.joinAndOpen(account.id, nick)} />
@@ -259,7 +256,7 @@ interface IrcChannelListing { } function join(accountId: string, name: string, store: ReturnType): 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 { - 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)} />

Kick chat is public, so this works signed out. Signing in adds talking, and your diff --git a/src/renderer/src/state/store.ts b/src/renderer/src/state/store.ts index 6a9f440..9cbde52 100644 --- a/src/renderer/src/state/store.ts +++ b/src/renderer/src/state/store.ts @@ -1306,6 +1306,17 @@ export class ChatStore { .catch((e: Error) => this.toast('error', e.message)) } + private groupsTimer: ReturnType | 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 { + // 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