Simplify how video call layout is stored

This commit is contained in:
mplorentz
2026-04-07 14:57:24 -04:00
committed by hodlbod
parent fb60d493b9
commit 2c4fe7bcf3
7 changed files with 93 additions and 77 deletions
+1 -1
View File
@@ -410,7 +410,7 @@ progress[value]::-webkit-progress-value {
transition: width 0.5s;
}
/* Anchors for fixed overlays (compose, search, reply) — main scroll lives in Page / PageContent flow */
/* content width for fixed elements */
.left-content {
@apply md:left-[calc(18.5rem+var(--sail))];
+21 -17
View File
@@ -11,23 +11,22 @@
import {
currentVoiceSession,
currentVoiceRoom,
VideoCallLayout,
videoCallLayoutRevision,
videoPrimaryTileKey,
toggleVideoPrimaryTile,
pubkeyFromLiveKitIdentity,
} from "@app/voice"
type Variant = "mobile" | "desktop-split" | "desktop-full"
type Props = {
variant: Variant
layout: VideoCallLayout
mobile?: boolean
url: string
h: string
visible?: boolean
class?: string
}
type Tile = {
type VideoTile = {
identity: string
isLocal: boolean
trackSid: string
@@ -37,11 +36,18 @@
type TileLayout = "spotlight" | "default" | "strip"
const {variant, url, h, visible = true, class: className = ""}: Props = $props()
const {layout, mobile = false, url, h, class: className = ""}: Props = $props()
const roomMatches = $derived($currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h)
const isViewingCurrentCallRoom = $derived(
$currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
)
const showPanel = $derived(visible && roomMatches)
const showPanel = $derived(
isViewingCurrentCallRoom &&
(mobile
? layout === VideoCallLayout.Video
: layout === VideoCallLayout.Split || layout === VideoCallLayout.Video),
)
const tiles = $derived.by(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- re-run when remote video subscribes
@@ -52,7 +58,7 @@
}
const room = session.room
const out: Tile[] = []
const out: VideoTile[] = []
const lp = room.localParticipant
if (session.cameraOn) {
@@ -104,7 +110,7 @@
})
/** Identity + source only — LiveKit can change trackSid after publish, which broke spotlight + stale-key effect. */
const tileKey = (t: Tile) => `${t.identity}\x1f${t.source}`
const tileKey = (t: VideoTile) => `${t.identity}\x1f${t.source}`
const primaryTile = $derived.by(() => {
const k = $videoPrimaryTileKey
@@ -137,7 +143,7 @@
}
})
const labelFor = (identity: string, source: Tile["source"]) => {
const labelFor = (identity: string, source: VideoTile["source"]) => {
const pk = pubkeyFromLiveKitIdentity(identity)
const name = pk ? displayProfileByPubkey(pk) : "Unknown"
return source === Track.Source.ScreenShare ? `${name} · screen` : name
@@ -151,18 +157,16 @@
const panelChrome = $derived(
cx(
variant === "mobile" &&
mobile &&
"flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden bg-base-200 px-2 pt-4 md:hidden pb-[calc(3.5rem+var(--saib))]",
variant === "desktop-split" &&
"flex min-h-0 w-full min-w-0 flex-1 flex-col gap-2 overflow-hidden bg-base-200 px-2 pb-2 pt-4",
variant === "desktop-full" &&
!mobile &&
"flex min-h-0 w-full min-w-0 flex-1 flex-col gap-2 overflow-hidden bg-base-200 px-2 pb-2 pt-4",
className,
),
)
</script>
{#snippet videoTile(tile: Tile, layout: TileLayout)}
{#snippet videoTile(tile: VideoTile, layout: TileLayout)}
<div
class={cx(
"relative isolate overflow-hidden rounded-box shadow-sm",
@@ -241,7 +245,7 @@
{#if showPanel}
<div class={panelChrome}>
{#if variant === "mobile"}
{#if mobile}
<div class="flex min-h-0 flex-1 flex-col gap-2">
<div class="min-h-0 flex-1 overflow-hidden">
{@render videoPanelBody()}
@@ -0,0 +1,28 @@
<script module lang="ts">
import {MediaQuery} from "svelte/reactivity"
export const isDesktopLayout = new MediaQuery("min-width: 768px", false)
</script>
<script lang="ts">
import {get} from "svelte/store"
import {VideoCallLayout, videoCallLayout} from "@app/voice"
let prevLayout = $state<boolean | undefined>(undefined)
$effect(() => {
const currentLayout = isDesktopLayout.current
if (prevLayout === undefined) {
prevLayout = currentLayout
return
}
if (prevLayout === currentLayout) return
const p = get(videoCallLayout)
if (prevLayout && !currentLayout) {
if (p === VideoCallLayout.Split) videoCallLayout.set(VideoCallLayout.Video)
} else if (!prevLayout && currentLayout) {
if (p === VideoCallLayout.Chat) videoCallLayout.set(VideoCallLayout.Split)
}
prevLayout = currentLayout
})
</script>
+18 -26
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {readable} from "svelte/store"
import {fade, fly} from "svelte/transition"
import {browser} from "$app/environment"
import {goto} from "$app/navigation"
import {page} from "$app/stores"
import cx from "classnames"
@@ -20,6 +19,7 @@
import Button from "@lib/components/Button.svelte"
import VoiceCallAudioSettingsDialog from "@app/components/VoiceCallAudioSettingsDialog.svelte"
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
import {isDesktopLayout} from "@app/components/VideoCallLayoutViewportSync.svelte"
import {
decodeRelay,
deriveRoom,
@@ -33,11 +33,11 @@
import {makeRoomPath} from "@app/util/routes"
import {
VoiceState,
VideoCallLayout,
currentVoiceSession,
currentVoiceRoom,
voiceState,
voiceMobileRoomPanel,
voiceDesktopRoomPanel,
videoCallLayout,
isLocalSpeaking,
leaveVoiceRoom,
toggleMute,
@@ -90,21 +90,6 @@
pushModal(VoiceCallAudioSettingsDialog)
}
let isMd = $state(
typeof window !== "undefined" && window.matchMedia("(min-width: 768px)").matches,
)
$effect(() => {
if (!browser) return
const mq = window.matchMedia("(min-width: 768px)")
const sync = () => {
isMd = mq.matches
}
sync()
mq.addEventListener("change", sync)
return () => mq.removeEventListener("change", sync)
})
const showVoiceLayoutToggle = $derived(
$voiceState === VoiceState.Connected &&
targetRoom !== undefined &&
@@ -115,17 +100,24 @@
h === targetRoom.h,
)
const layoutToggleActive = $derived(
const isChatPanelActive = $derived(
showVoiceLayoutToggle &&
((!isMd && $voiceMobileRoomPanel === "chat") || (isMd && $voiceDesktopRoomPanel === "split")),
((!isDesktopLayout.current &&
($videoCallLayout === VideoCallLayout.Chat ||
$videoCallLayout === VideoCallLayout.Split)) ||
(isDesktopLayout.current && $videoCallLayout === VideoCallLayout.Split)),
)
const onLayoutToggle = () => {
const onChatToggle = () => {
if (!showVoiceLayoutToggle) return
if (isMd) {
voiceDesktopRoomPanel.update(p => (p === "split" ? "video" : "split"))
if (isDesktopLayout.current) {
videoCallLayout.update(p =>
p === VideoCallLayout.Split ? VideoCallLayout.Video : VideoCallLayout.Split,
)
} else {
voiceMobileRoomPanel.update(p => (p === "chat" ? "video" : "chat"))
videoCallLayout.update(p =>
p === VideoCallLayout.Video ? VideoCallLayout.Chat : VideoCallLayout.Video,
)
}
}
@@ -177,9 +169,9 @@
class={cx(
mediaToggleClass,
"relative shrink-0 overflow-visible",
layoutToggleActive && "text-primary",
isChatPanelActive && "text-primary",
)}
onclick={onLayoutToggle}>
onclick={onChatToggle}>
<span class="relative inline-flex">
<Icon icon={ChatRound} size={4} />
{#if chatUnread}
+11 -9
View File
@@ -87,15 +87,17 @@ export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
export const currentVoiceRoom = writable<Room | undefined>(undefined)
/** Mobile room UI: full-screen chat vs video (see VoiceWidget layout toggle). */
export const voiceMobileRoomPanel = writable<"chat" | "video">("chat")
/** Chat-only, full-width video, or split (desktop). On narrow viewports, `split` shows as chat until resize remaps it. */
export enum VideoCallLayout {
Chat = "chat",
Video = "video",
Split = "split",
}
/** Desktop room UI: messages only, video only, or split (see VoiceWidget layout toggle). */
export const voiceDesktopRoomPanel = writable<"chat" | "video" | "split">("split")
export const videoCallLayout = writable<VideoCallLayout>(VideoCallLayout.Split)
const resetVoiceRoomPanels = () => {
voiceMobileRoomPanel.set("chat")
voiceDesktopRoomPanel.set("chat")
const resetVideoCallLayout = () => {
videoCallLayout.set(VideoCallLayout.Chat)
}
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
@@ -240,7 +242,7 @@ const onRoomDisconnected = (reason?: DisconnectReason) => {
videoCallLayoutRevision.set(0)
videoPrimaryTileKey.set(undefined)
currentVoiceSession.set(undefined)
resetVoiceRoomPanels()
resetVideoCallLayout()
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
voiceState.set(VoiceState.Disconnected)
const message =
@@ -408,7 +410,7 @@ export const leaveVoiceRoom = async () => {
videoCallLayoutRevision.set(0)
videoPrimaryTileKey.set(undefined)
currentVoiceSession.set(undefined)
resetVoiceRoomPanels()
resetVideoCallLayout()
session.room.disconnect()
speakingParticipants.set([])
participantPubkeyMap.set(new Map())
+2
View File
@@ -42,6 +42,7 @@
import {syncKeyboard} from "@app/util/keyboard"
import {getPageTitle} from "@app/util/title"
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
import VideoCallLayoutViewportSync from "@app/components/VideoCallLayoutViewportSync.svelte"
const {children} = $props()
@@ -229,5 +230,6 @@
<ModalContainer />
<div class="tippy-target"></div>
<NewNotificationSound />
<VideoCallLayoutViewportSync />
</div>
{/await}
+12 -24
View File
@@ -47,10 +47,10 @@
import VideoCallContent from "@app/components/VideoCallContent.svelte"
import {
VoiceState,
VideoCallLayout,
currentVoiceRoom,
videoTileCount,
voiceMobileRoomPanel,
voiceDesktopRoomPanel,
videoCallLayout,
voiceState,
} from "@app/voice"
import {makeFeed} from "@app/core/requests"
@@ -74,19 +74,20 @@
)
const showMobileVideoPanel = $derived(
isVoiceRoom && $voiceState === VoiceState.Connected && $voiceMobileRoomPanel === "video",
isVoiceRoom &&
$voiceState === VoiceState.Connected &&
$videoCallLayout === VideoCallLayout.Video,
)
const pageContentHiddenDesktopVideoOnly = $derived(
voiceConnectedHere && $voiceDesktopRoomPanel === "video",
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video,
)
let prevVideoTileCount = $state(0)
$effect(() => {
if ($voiceState !== VoiceState.Connected) {
voiceMobileRoomPanel.set("chat")
voiceDesktopRoomPanel.set("chat")
videoCallLayout.set(VideoCallLayout.Chat)
prevVideoTileCount = 0
return
}
@@ -100,8 +101,7 @@
}
if (prevVideoTileCount === 0 && n >= 1) {
voiceDesktopRoomPanel.set("video")
voiceMobileRoomPanel.set("video")
videoCallLayout.set(VideoCallLayout.Video)
}
prevVideoTileCount = n
})
@@ -416,35 +416,23 @@
<div
class={cx(
"flex min-h-0 flex-1 flex-col",
voiceConnectedHere && $voiceDesktopRoomPanel === "split" && "md:flex-row",
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Split && "md:flex-row",
)}>
{#if voiceConnectedHere}
<VideoCallContent
variant="desktop-split"
layout={$videoCallLayout}
{url}
{h}
visible={$voiceDesktopRoomPanel === "split"}
class="hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex" />
<VideoCallContent
variant="desktop-full"
{url}
{h}
visible={$voiceDesktopRoomPanel === "video"}
class="hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex" />
{/if}
<div
class={cx(
"flex min-h-0 min-w-0 flex-1 flex-col",
voiceConnectedHere && $voiceDesktopRoomPanel === "video" && "md:hidden",
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video && "md:hidden",
)}>
{#if isVoiceRoom && $voiceState === VoiceState.Connected}
<VideoCallContent
variant="mobile"
{url}
{h}
visible={$voiceMobileRoomPanel === "video"}
class="md:hidden" />
<VideoCallContent layout={$videoCallLayout} mobile {url} {h} class="md:hidden" />
{/if}
<PageContent