Discord calls: per-account camera and share, sticker grid at sticker size
A window can be in two accounts' calls. The camera and the screen share are now each held against the account they belong to, the stage's buttons act on the stage's own account, and a share is stopped through the conversation it was started in. Previously the share button took the first Discord call in the window, whichever account that was. Stickers were drawn at emoji size in an emoji-width grid; the cell now lifts the emoji cap and a grid of stickers gets 64px columns. The Go Live chooser's segments sit at their own width. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: ac6ef9cbb5...7b6441497f
@@ -2890,6 +2890,12 @@ img.smilie {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Stickers are bigger than emoji, so their columns are too. Chosen by what
|
||||
the grid holds rather than by a prop, so every caller gets it. */
|
||||
.emoji-grid:has(> .sticker-cell) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
|
||||
}
|
||||
|
||||
/* Holds a section's full height while only the rows near the viewport are
|
||||
built inside it, so the sections below keep their place and the scrollbar
|
||||
does not twitch as rows come and go. The grid it wraps is shifted down to
|
||||
@@ -3877,6 +3883,9 @@ img.smilie {
|
||||
.emoji-cell.sticker-cell img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* An emoji's cap would draw a sticker at emoji size in a sticker's cell. */
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -7346,8 +7355,14 @@ video.lightbox-media {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.go-live > .button.primary {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.segmented {
|
||||
display: inline-flex;
|
||||
align-self: flex-start;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid var(--outline-medium);
|
||||
border-radius: var(--corner-radius-small);
|
||||
|
||||
@@ -77,8 +77,10 @@ export function DiscordStage({ session, where }: { session: VoiceSession; where:
|
||||
const store = useStore()
|
||||
const buffers = useChat((s) => s.buffers)
|
||||
const cameras = useChat((s) => s.discordCameras)
|
||||
const sharing = useChat((s) => s.discordSharing)
|
||||
const ownCamera = useChat((s) => s.discordCamera)
|
||||
const sharing = useChat((s) => s.discordSharing) === session.accountId
|
||||
const myCamera = useChat((s) => s.discordCamera)
|
||||
// Only this call's account: one window can be in two accounts' calls.
|
||||
const ownCamera = myCamera?.accountId === session.accountId ? myCamera.stream : null
|
||||
const [choosing, setChoosing] = useState(false)
|
||||
const watching = useChat((s) => s.discordWatching)
|
||||
const voicePrefs = useChat((s) => s.voicePrefs)
|
||||
@@ -176,6 +178,13 @@ export function DiscordStage({ session, where }: { session: VoiceSession; where:
|
||||
}
|
||||
}
|
||||
|
||||
// From this call's own account. The window can be in two accounts' calls,
|
||||
// and asking the store for "the" Discord call picks whichever came first.
|
||||
const share = (quality?: VideoQuality): Promise<void> =>
|
||||
session.bufferId
|
||||
? store.toggleDiscordScreenShare(accountId, session.bufferId, quality)
|
||||
: store.toggleScreenShare(quality)
|
||||
|
||||
const buttons: StageButton[] = [
|
||||
{
|
||||
icon: voicePrefs.micMuted ? 'mic_off' : 'mic',
|
||||
@@ -201,14 +210,14 @@ export function DiscordStage({ session, where }: { session: VoiceSession; where:
|
||||
active: sharing,
|
||||
// Stopping needs no questions; starting asks how good a stream, the
|
||||
// way Discord's own client does, beside the button that asked.
|
||||
onClick: () => (sharing ? void store.toggleScreenShare() : setChoosing(!choosing)),
|
||||
onClick: () => (sharing ? void share() : setChoosing(!choosing)),
|
||||
popover:
|
||||
choosing && !sharing ? (
|
||||
<GoLiveChooser
|
||||
accountId={accountId}
|
||||
onStart={(quality) => {
|
||||
setChoosing(false)
|
||||
void store.toggleScreenShare(quality)
|
||||
void share(quality)
|
||||
}}
|
||||
onClose={() => setChoosing(false)}
|
||||
/>
|
||||
|
||||
@@ -538,13 +538,17 @@ export interface ChatState {
|
||||
discordModal: DiscordModal | null
|
||||
/** The screens and windows on offer, while somebody is choosing one. */
|
||||
screenSources: { id: string; name: string; thumbnail: string }[] | null
|
||||
/** Whether this window is sharing a screen into a Discord call. */
|
||||
discordSharing: boolean
|
||||
/**
|
||||
* Which account this window is sharing a screen from into a Discord call,
|
||||
* if any. An account rather than a flag: one window can be in two
|
||||
* accounts' calls, and only one of them is sharing.
|
||||
*/
|
||||
discordSharing: string | null
|
||||
/**
|
||||
* This end's camera in a Discord call, while it is on: the capture, so the
|
||||
* call can show you what everybody else sees.
|
||||
*/
|
||||
discordCamera: MediaStream | null
|
||||
discordCamera: { accountId: string; stream: MediaStream } | null
|
||||
/**
|
||||
* Who is streaming right now, by Discord user id, and the key their stream
|
||||
* is named by.
|
||||
@@ -672,7 +676,7 @@ const INITIAL: ChatState = {
|
||||
screenSources: null,
|
||||
discordModal: null,
|
||||
activeCall: null,
|
||||
discordSharing: false,
|
||||
discordSharing: null,
|
||||
discordCamera: null,
|
||||
discordStreams: {},
|
||||
discordCameras: {},
|
||||
@@ -937,6 +941,8 @@ export class ChatStore {
|
||||
* `discordSharing` beside it.
|
||||
*/
|
||||
private discordShare: { stop: () => void; stream: MediaStream } | null = null
|
||||
/** The conversation the share was started against, which is what stops it. */
|
||||
private discordShareBuffer: string | null = null
|
||||
/** The Discord camera's encoder, held for the same reason. */
|
||||
private discordCameraSend: { stop: () => void; stream: MediaStream } | null = null
|
||||
/**
|
||||
@@ -1775,11 +1781,11 @@ export class ChatStore {
|
||||
// What this end was sending goes with it. Left running, the camera light
|
||||
// would stay on and the encoder would go on handing frames to a call
|
||||
// that has ended.
|
||||
this.stopDiscordCamera()
|
||||
if (this.discordShare) {
|
||||
if (this.state.discordCamera?.accountId === accountId) this.stopDiscordCamera(accountId)
|
||||
if (this.discordShare && this.state.discordSharing === accountId) {
|
||||
this.discordShare.stop()
|
||||
this.discordShare = null
|
||||
this.set({ discordSharing: false })
|
||||
this.set({ discordSharing: null })
|
||||
}
|
||||
try {
|
||||
await window.moho.rpc('leaveVoiceChannel', { accountId })
|
||||
@@ -2559,12 +2565,16 @@ export class ChatStore {
|
||||
*/
|
||||
async toggleDiscordScreenShare(accountId: string, bufferId: string, quality: VideoQuality = DEFAULT_QUALITY): Promise<void> {
|
||||
if (this.discordShare) {
|
||||
const was = this.state.discordSharing
|
||||
this.discordShare.stop()
|
||||
this.discordShare = null
|
||||
this.set({ discordSharing: false })
|
||||
await window.moho.rpc('stopDiscordScreenShare', { bufferId }).catch(() => {})
|
||||
return
|
||||
this.set({ discordSharing: null })
|
||||
await window.moho.rpc('stopDiscordScreenShare', { bufferId: this.discordShareBuffer ?? bufferId }).catch(() => {})
|
||||
// One screen, one stream: pressing share in another account's call
|
||||
// moves it there rather than only stopping it.
|
||||
if (was === accountId) return
|
||||
}
|
||||
this.discordShareBuffer = bufferId
|
||||
|
||||
try {
|
||||
// The screen first, then Discord. The person chooses while the click
|
||||
@@ -2600,10 +2610,10 @@ export class ChatStore {
|
||||
this.discordShare = await shareScreen(accountId, stream, settings, (message) => {
|
||||
this.toast('error', message)
|
||||
this.discordShare = null
|
||||
this.set({ discordSharing: false })
|
||||
this.set({ discordSharing: null })
|
||||
void window.moho.rpc('stopDiscordScreenShare', { bufferId }).catch(() => {})
|
||||
})
|
||||
this.set({ discordSharing: true })
|
||||
this.set({ discordSharing: accountId })
|
||||
} catch (e) {
|
||||
this.toast('error', `Couldn't share the screen: ${(e as Error).message}`)
|
||||
await window.moho.rpc('stopDiscordScreenShare', { bufferId }).catch(() => {})
|
||||
@@ -2620,9 +2630,12 @@ export class ChatStore {
|
||||
* can read.
|
||||
*/
|
||||
async toggleDiscordCamera(accountId: string): Promise<void> {
|
||||
if (this.state.discordCamera) {
|
||||
this.stopDiscordCamera(accountId)
|
||||
return
|
||||
const current = this.state.discordCamera
|
||||
if (current) {
|
||||
this.stopDiscordCamera(current.accountId)
|
||||
// One camera, one call: pressing it in a second account's call moves
|
||||
// it there rather than only turning it off.
|
||||
if (current.accountId === accountId) return
|
||||
}
|
||||
let stream: MediaStream
|
||||
try {
|
||||
@@ -2660,7 +2673,7 @@ export class ChatStore {
|
||||
this.set({ discordCamera: null })
|
||||
giveUp(message)
|
||||
})
|
||||
this.set({ discordCamera: stream })
|
||||
this.set({ discordCamera: { accountId, stream } })
|
||||
} catch (e) {
|
||||
giveUp(`Couldn't turn the camera on: ${(e as Error).message}`)
|
||||
}
|
||||
@@ -2671,10 +2684,11 @@ export class ChatStore {
|
||||
const sending = this.discordCameraSend
|
||||
this.discordCameraSend = null
|
||||
sending?.stop()
|
||||
const stream = this.state.discordCamera
|
||||
if (stream) for (const track of stream.getTracks()) track.stop()
|
||||
if (stream || sending) this.set({ discordCamera: null })
|
||||
if (accountId) void window.moho.rpc('setDiscordCamera', { accountId, on: false }).catch(() => {})
|
||||
const camera = this.state.discordCamera
|
||||
if (camera) for (const track of camera.stream.getTracks()) track.stop()
|
||||
if (camera || sending) this.set({ discordCamera: null })
|
||||
const whose = accountId ?? camera?.accountId
|
||||
if (whose) void window.moho.rpc('setDiscordCamera', { accountId: whose, on: false }).catch(() => {})
|
||||
}
|
||||
|
||||
/** Waits for the daemon to say the stream connection is up. */
|
||||
|
||||
Reference in New Issue
Block a user