Simplify how video call layout is stored
This commit is contained in:
+1
-1
@@ -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))];
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user