A Kick channel's header says what is on air
The chat is watched beside a video moho does not show, so the header carries the context that video would: a red dot while live, the stream's title, the category, and how many people are watching. Offline says so outright, because a channel that is quiet and one that is off air look identical otherwise. Each part shrinks before the row does, so a long stream title cannot push the buttons beside it off the header.
This commit is contained in:
+1
-1
Submodule nobilis updated: 20d087d077...39f8ae3912
@@ -4629,3 +4629,39 @@ video.lightbox-media {
|
||||
margin-left: auto;
|
||||
max-width: 55%;
|
||||
}
|
||||
|
||||
/* --- what a Kick channel is broadcasting ---------------------------------
|
||||
The chat is watched beside a video this client does not show, so the header
|
||||
carries the context that video would: what the stream is, what is being
|
||||
played, and how many people are there. */
|
||||
.stream-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
min-width: 0;
|
||||
max-width: 42%;
|
||||
padding: 1px var(--spacing-xs);
|
||||
border: 1px solid var(--outline-light);
|
||||
border-radius: var(--corner-radius-small);
|
||||
color: var(--surface-variant-text);
|
||||
}
|
||||
|
||||
.stream-chip.live {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
/* Red, and only present while live - the one thing on the chip that must be
|
||||
readable at a glance from across a room. */
|
||||
.stream-dot {
|
||||
flex: none;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
/* Each part shrinks before the row does, so a long stream title cannot push
|
||||
the buttons beside it off the header. */
|
||||
.stream-chip .ellipsis {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { Icon, IconButton } from './Icon'
|
||||
import { AddToConversation } from './AddToConversation'
|
||||
import { useChat, useStore } from '../state/hooks'
|
||||
import { formatFullTime } from '../lib/util'
|
||||
import { classes, formatFullTime } from '../lib/util'
|
||||
import type { BufferEntry } from '../state/store'
|
||||
import type { Message } from '../../../shared/wire'
|
||||
|
||||
@@ -110,6 +110,7 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
const box = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isDiscord = buffer.accountId.startsWith('discord:')
|
||||
const stream = useChat((s) => s.kickStreams)[buffer.id]
|
||||
const isDm = buffer.kind === 'dm'
|
||||
// This conversation's call, not merely a call on the same account: one
|
||||
// account can only be in one, but the button has to be right about which.
|
||||
@@ -203,6 +204,37 @@ export function ConversationTools({ buffer }: { buffer: BufferEntry }): JSX.Elem
|
||||
{buffer.channelModes}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* What the stream is, for a channel that is one. Kick's chat is
|
||||
watched beside a video this client does not show, so the title, the
|
||||
game and the number of people watching are the context the chat is
|
||||
missing - and "offline" is worth saying too, since a quiet channel
|
||||
and an off-air one look identical otherwise. */}
|
||||
{stream && (
|
||||
<span
|
||||
className={classes('stream-chip', 'small', stream.live && 'live')}
|
||||
title={
|
||||
stream.live
|
||||
? [stream.title, stream.category, stream.viewers ? `${stream.viewers.toLocaleString()} watching` : null]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
: 'Not streaming right now'
|
||||
}
|
||||
>
|
||||
{stream.live ? (
|
||||
<>
|
||||
<span className="stream-dot" />
|
||||
<span className="ellipsis">{stream.title || 'Live'}</span>
|
||||
{stream.category && <span className="muted ellipsis">{stream.category}</span>}
|
||||
{stream.viewers !== null && stream.viewers !== undefined && (
|
||||
<span className="muted">{stream.viewers.toLocaleString()}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="muted">offline</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{isDiscord && isDm && (
|
||||
<IconButton
|
||||
name={inCall ? 'call_end' : 'call'}
|
||||
|
||||
@@ -106,6 +106,18 @@ function autojoinList(autojoin: string | undefined): string[] {
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
/** What a Kick channel is broadcasting, as the daemon last asked. */
|
||||
export interface KickStream {
|
||||
bufferId: string
|
||||
live: boolean
|
||||
title?: string | null
|
||||
category?: string | null
|
||||
viewers?: number | null
|
||||
/** Unix seconds the stream started, for "live for 2h 14m". */
|
||||
startedTs?: number | null
|
||||
followers?: number | null
|
||||
}
|
||||
|
||||
export type ActivePanel = '' | 'accounts' | 'settings' | 'join' | 'downloads'
|
||||
|
||||
export interface ChatState {
|
||||
@@ -232,6 +244,14 @@ export interface ChatState {
|
||||
/** Per-buffer "New messages" divider timestamp, snapshotted on open. */
|
||||
dividerTsByBuffer: Record<string, number>
|
||||
matrixPermissions: Record<string, RoomPermissions>
|
||||
/**
|
||||
* What each Kick channel is broadcasting, by buffer.
|
||||
*
|
||||
* A Kick channel is a stream as much as a chat, and moho showed only the
|
||||
* chat - so the title, the game and the number of people watching, which is
|
||||
* most of what a viewer wants to know, were nowhere.
|
||||
*/
|
||||
kickStreams: Record<string, KickStream>
|
||||
/**
|
||||
* The profile being shown, or null.
|
||||
*
|
||||
@@ -302,6 +322,7 @@ const INITIAL: ChatState = {
|
||||
readersByBuffer: {},
|
||||
openThread: null,
|
||||
matrixPermissions: {},
|
||||
kickStreams: {},
|
||||
profile: null,
|
||||
replyingTo: null,
|
||||
toasts: [],
|
||||
@@ -1388,6 +1409,12 @@ export class ChatStore {
|
||||
break
|
||||
}
|
||||
|
||||
case 'kickStream': {
|
||||
const stream = data as unknown as KickStream
|
||||
this.set({ kickStreams: { ...this.state.kickStreams, [stream.bufferId]: stream } })
|
||||
break
|
||||
}
|
||||
|
||||
case 'presenceChange':
|
||||
this.set({
|
||||
presenceByBuffer: { ...this.state.presenceByBuffer, [data.bufferId]: data.members }
|
||||
|
||||
Reference in New Issue
Block a user