A kick.com link opens the stream as well as the chat

Clicking one used to swallow the link: the chat opened here and the
stream went nowhere, so somebody clicking through to watch ended up with
half of what they asked for. It does both now - the stream in the
browser, where video is watched, and the chat here, which is what this
client is for.

With no Kick account the stream still opens and the toast says only the
chat is missing, rather than reporting a failure for something that
half-worked.

And a Kick channel's own row has "Open in browser", next to the way to
open its chat in a window - a channel is a stream as well as a chat, and
the stream is the part moho does not show.
This commit is contained in:
2026-09-03 18:40:54 -04:00
parent e0b063e7a2
commit eb45b4a6ea
4 changed files with 33 additions and 9 deletions
+1 -1
Submodule nobilis updated: 1bd675c32c...20d087d077
@@ -277,6 +277,11 @@ export function BufferList(): JSX.Element {
: undefined
}
autojoins={store.autojoins(b.accountId, bufferDisplayName(b.name))}
onOpenInBrowser={
b.accountId.startsWith('kick:')
? () => void window.moho.openExternal(`https://kick.com/${bufferDisplayName(b.name)}`)
: undefined
}
onHide={() => hideBuffer(b.id)}
onClose={() => void store.closeBuffer(b.id)}
inCall={voiceSessions.some((s) => s.bufferId === b.id)}
@@ -678,6 +683,8 @@ interface BufferRowProps {
onToggleMute: () => void
/** IRC channels only: rejoin this on every connect, or stop doing so. */
onToggleAutojoin?: () => void
/** Kick channels only: watch the stream where streams are watched. */
onOpenInBrowser?: () => void
/** Whether it is in that list now. */
autojoins?: boolean
onHide: () => void
@@ -715,6 +722,7 @@ function BufferRow({
onToggleMute,
onToggleAutojoin,
autojoins,
onOpenInBrowser,
onHide,
onClose,
onCall,
@@ -776,6 +784,12 @@ function BufferRow({
poppedOut
? { label: 'Close its window', icon: 'close_fullscreen', onClick: onDock }
: { label: 'Open in a new window', icon: 'open_in_new', onClick: onPopOut },
// A Kick channel is a stream as well as a chat, and the stream is not
// something this client shows - so the way to watch it belongs on the
// row, next to the way to open its chat in a window.
...(onOpenInBrowser
? ([{ label: 'Open in browser', icon: 'public', onClick: onOpenInBrowser }] as MenuEntry[])
: []),
{ separator: true },
// Which channels an account rejoins on connect is a property of the
// channels, so it is set on one - it used to be a comma-separated field
+5 -2
View File
@@ -216,8 +216,11 @@ function walk(node: Node, handlers: Handlers, depth: number): ReactNode[] {
break
}
// A link to a channel is handled here rather than handed to the OS,
// which would bounce it straight back to this window.
// A link to a channel is acted on here rather than handed straight to
// the OS. What that means differs by service: an irc: link belongs to
// this client alone, while a kick.com link is a web page as well as a
// channel, so following it opens the stream in the browser *and* the
// chat here - see the store's followDeepLink.
if (isDeepLink(href)) {
out.push(
<a
+13 -6
View File
@@ -884,17 +884,24 @@ export class ChatStore {
return
}
// A kick.com link names a streamer, and watching one needs no more than
// that - so it opens the channel rather than the browser. Any Kick
// account will do: they all read the same public chat, and the signed-in
// one is preferred only because it can also talk.
// A kick.com link names a streamer, and clicking one means both things:
// the stream goes to the browser, which is where video is watched, and
// the chat opens here, which is what this client is for. Doing only the
// second was the old behaviour and it swallowed the link - somebody
// clicking through to watch ended up with a chat and no stream.
//
// Any Kick account will do for the chat: they all read the same public
// one, and the signed-in one is preferred only because it can also talk.
if (link.service === 'kick') {
void window.moho.openExternal(url)
const account =
this.state.accounts.find((a) => a.service === 'kick' && a.hasPassword) ??
this.state.accounts.find((a) => a.service === 'kick')
if (!account) {
this.toast('error', 'Add a Kick account to open that link here')
void window.moho.openExternal(url)
// The stream is already opening; only the chat is missing, and
// saying so is better than a silence that looks like half of it
// failed.
this.toast('info', 'Add a Kick account to open that chat here too')
return
}
this.set({ activePanel: '' })