Clean up voice room/chat layout
This commit is contained in:
parent
2974faa2e9
commit
31890dfe90
6 changed files with 138 additions and 329 deletions
|
|
@ -39,12 +39,9 @@
|
|||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
// suppress the full in-call control row on desktop — used where a wider,
|
||||
// unobstructed copy of it already floats over the video panel
|
||||
hideConnectedOnDesktop?: boolean
|
||||
}
|
||||
|
||||
const {url, h, hideConnectedOnDesktop = false}: Props = $props()
|
||||
const {url, h}: Props = $props()
|
||||
|
||||
const roomStore = $derived($rooms.forRoom(url, h))
|
||||
const room = $derived($roomStore)
|
||||
|
|
@ -67,15 +64,7 @@
|
|||
const chatUnread = $derived($notifications.has(roomPath))
|
||||
const isChatPanelActive = $derived($videoCallLayout === VideoCallLayout.Split)
|
||||
|
||||
// With chat open, the floating bar shares the video pane with the sidebar rather
|
||||
// than spanning the full width — a smaller bar reads as sized for that narrower
|
||||
// space instead of just looking unchanged. Sized below mobile's max-md:h-9 (36px)
|
||||
// since desktop is mouse-driven, not a touch target. max-md sizing itself is
|
||||
// unaffected: mobile chat is a full-screen overlay, not a space-sharing sidebar.
|
||||
const compactButtonClass = $derived(
|
||||
cx("max-md:h-9 max-md:w-9", isChatPanelActive && "md:h-8 md:w-8"),
|
||||
)
|
||||
const iconSize = $derived(isChatPanelActive ? 3.5 : 4.5)
|
||||
const buttonClass = "button button-circle max-md:h-9 max-md:w-9"
|
||||
|
||||
const onChatToggle = () => {
|
||||
videoCallLayout.update(p =>
|
||||
|
|
@ -86,15 +75,13 @@
|
|||
|
||||
{#if joiningHere || connectedHere || showJoin}
|
||||
<!-- every state shares this grid cell so switching between them cross-fades in
|
||||
place instead of pushing/jumping past each other as flex siblings, and the
|
||||
grid auto-sizes to whichever state is actually present (nothing reserves
|
||||
layout space when e.g. the connected bar is display:none on desktop). -->
|
||||
place instead of pushing/jumping past each other as flex siblings. -->
|
||||
<div class="grid items-center justify-center">
|
||||
{#if joiningHere}
|
||||
<div
|
||||
in:fade={{duration: 160}}
|
||||
out:fade={{duration: 120}}
|
||||
class="pointer-events-auto col-start-1 row-start-1 flex h-10 items-center gap-1.5 rounded-full border border-line bg-surface/95 px-1.5 shadow-xl backdrop-blur-md">
|
||||
class="col-start-1 row-start-1 flex h-10 items-center gap-1.5 rounded-full border border-line bg-surface/95 px-1.5 shadow-xl backdrop-blur-md">
|
||||
<span
|
||||
data-tip="Joining call…"
|
||||
role="status"
|
||||
|
|
@ -113,22 +100,14 @@
|
|||
<div
|
||||
in:fade={{duration: 160}}
|
||||
out:fade={{duration: 120}}
|
||||
class={cx(
|
||||
"pointer-events-auto col-start-1 row-start-1 flex items-center gap-1.5 rounded-full border border-line bg-surface/95 p-1.5 shadow-xl backdrop-blur-md md:gap-2 md:p-2",
|
||||
isChatPanelActive && "md:gap-1 md:p-1",
|
||||
hideConnectedOnDesktop && "md:hidden",
|
||||
)}>
|
||||
class="col-start-1 row-start-1 flex items-center gap-1.5 rounded-full border border-line bg-surface/95 p-1.5 shadow-xl backdrop-blur-md md:gap-2 md:p-2">
|
||||
<Button
|
||||
data-tip={$callMicMuted ? "Unmute" : "Mute"}
|
||||
aria-label={$callMicMuted ? "Unmute microphone" : "Mute microphone"}
|
||||
aria-pressed={!$callMicMuted}
|
||||
class={cx(
|
||||
"button button-circle",
|
||||
compactButtonClass,
|
||||
$callMicMuted ? "button-neutral" : "button-primary",
|
||||
)}
|
||||
class={cx(buttonClass, $callMicMuted ? "button-neutral" : "button-primary")}
|
||||
onclick={toggleMute}>
|
||||
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={iconSize} />
|
||||
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={4.5} />
|
||||
</Button>
|
||||
{#if $currentCallSession}
|
||||
<Button
|
||||
|
|
@ -136,14 +115,13 @@
|
|||
aria-label={$currentCallSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
||||
aria-pressed={$currentCallSession.cameraOn}
|
||||
class={cx(
|
||||
"button button-circle",
|
||||
compactButtonClass,
|
||||
buttonClass,
|
||||
$currentCallSession.cameraOn ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={toggleCamera}>
|
||||
<Icon
|
||||
icon={$currentCallSession.cameraOn ? VideocameraRecord : VideocameraOff}
|
||||
size={iconSize} />
|
||||
size={4.5} />
|
||||
</Button>
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<Button
|
||||
|
|
@ -153,33 +131,32 @@
|
|||
: "Share screen"}
|
||||
aria-pressed={$currentCallSession.screenShareOn}
|
||||
class={cx(
|
||||
"button button-circle",
|
||||
compactButtonClass,
|
||||
buttonClass,
|
||||
$currentCallSession.screenShareOn ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={toggleScreenShare}>
|
||||
<Icon icon={Monitor} size={iconSize} />
|
||||
<Icon icon={Monitor} size={4.5} />
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
data-tip="Call settings"
|
||||
aria-label="Call settings"
|
||||
class={cx("button button-circle button-neutral", compactButtonClass)}
|
||||
class={cx(buttonClass, "button-neutral")}
|
||||
onclick={openCallSettings}>
|
||||
<Icon icon={Settings} size={iconSize} />
|
||||
<Icon icon={Settings} size={4.5} />
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Toggle chat"
|
||||
aria-label="Toggle chat panel"
|
||||
aria-pressed={isChatPanelActive}
|
||||
class={cx(
|
||||
"button button-circle relative",
|
||||
compactButtonClass,
|
||||
buttonClass,
|
||||
"relative",
|
||||
isChatPanelActive ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={onChatToggle}>
|
||||
<Icon icon={ChatRound} size={iconSize} />
|
||||
<Icon icon={ChatRound} size={4.5} />
|
||||
{#if chatUnread}
|
||||
<span
|
||||
class="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-primary ring-2 ring-surface"
|
||||
|
|
@ -190,17 +167,14 @@
|
|||
<Button
|
||||
data-tip="Leave call"
|
||||
aria-label="Leave voice room"
|
||||
class={cx("button button-circle button-error", compactButtonClass)}
|
||||
class={cx(buttonClass, "button-error")}
|
||||
onclick={leaveVoiceRoom}>
|
||||
<Icon icon={EndCall} size={isChatPanelActive ? 4 : 5} />
|
||||
<Icon icon={EndCall} size={5} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showJoin}
|
||||
<div
|
||||
in:fade={{duration: 160}}
|
||||
out:fade={{duration: 120}}
|
||||
class="pointer-events-auto col-start-1 row-start-1">
|
||||
<div in:fade={{duration: 160}} out:fade={{duration: 120}} class="col-start-1 row-start-1">
|
||||
<Button
|
||||
data-tip="Join call"
|
||||
aria-label="Join voice room"
|
||||
|
|
|
|||
|
|
@ -32,14 +32,7 @@
|
|||
import CallControlBar from "@app/components/CallControlBar.svelte"
|
||||
import {deletes, relays, rooms, thunks, user} from "@app/core"
|
||||
import {joinRoom, leaveRoom} from "@app/access"
|
||||
import {
|
||||
CallState,
|
||||
callTargetRoom,
|
||||
callState,
|
||||
VideoCallLayout,
|
||||
videoCallLayout,
|
||||
deriveIsCallActiveElsewhere,
|
||||
} from "@app/call"
|
||||
import {CallState, callTargetRoom, callState, VideoCallLayout, videoCallLayout} from "@app/call"
|
||||
import {
|
||||
PROTECTED,
|
||||
RoomType,
|
||||
|
|
@ -71,40 +64,22 @@
|
|||
isVoiceRoom && $callState === CallState.Connected && isCallTargetingThisRoom,
|
||||
)
|
||||
|
||||
const isCallActiveElsewhere = $derived(deriveIsCallActiveElsewhere(url, h))
|
||||
const callBannerVisible = $derived($isCallActiveElsewhere)
|
||||
|
||||
// Reuses voiceConnectedHere (rather than re-deriving isVoiceRoom/callState) so it
|
||||
// can't diverge and stay true for a different voice room the call isn't targeting
|
||||
// — that previously hid this room's messages/compose row while connected elsewhere.
|
||||
const showMobileVideoPanel = $derived(
|
||||
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video,
|
||||
)
|
||||
|
||||
// Basic pass at issue #121's "chat overlaid on video" idea: float the chat panel over
|
||||
// the full-bleed video instead of splitting the screen into fixed columns, so the call
|
||||
// stays the visual focus. The panel opens from — and collapses back to — a chat FAB.
|
||||
const isOverlayChat = $derived(voiceConnectedHere && $videoCallLayout === VideoCallLayout.Split)
|
||||
|
||||
const videoCallChatHidden = $derived(
|
||||
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video,
|
||||
)
|
||||
// During a call the chat is toggled from the call controls — it takes over the pane,
|
||||
// or sits beside the video once there's room for both. Reuses voiceConnectedHere
|
||||
// rather than re-deriving isVoiceRoom/callState so it can't stay true for a voice
|
||||
// room the call isn't targeting, which would hide that room's messages.
|
||||
const callChatOpen = $derived(voiceConnectedHere && $videoCallLayout === VideoCallLayout.Split)
|
||||
const callChatHidden = $derived(voiceConnectedHere && !callChatOpen)
|
||||
|
||||
const roomPath = h ? makeRoomPath(url, h) : undefined
|
||||
|
||||
const closeChat = () => videoCallLayout.set(VideoCallLayout.Video)
|
||||
|
||||
$effect(() => {
|
||||
deferredRoomPath.set(videoCallChatHidden ? roomPath : undefined)
|
||||
if (roomPath && voiceConnectedHere && !videoCallChatHidden) {
|
||||
deferredRoomPath.set(callChatHidden ? roomPath : undefined)
|
||||
if (roomPath && callChatOpen) {
|
||||
setChecked(roomPath)
|
||||
}
|
||||
// The messages pane (and its scroll position) is offscreen while the call view is
|
||||
// showing — don't leave its floating "scroll down"/"new messages" chips stranded.
|
||||
if (videoCallChatHidden) {
|
||||
showScrollButton = false
|
||||
showFixedNewMessages = false
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => deferredRoomPath.set(undefined))
|
||||
|
|
@ -120,8 +95,8 @@
|
|||
return
|
||||
}
|
||||
|
||||
// Land on the call view (chat as a FAB) as soon as the call starts here, whether
|
||||
// or not anyone's camera is on — chat is one tap away via the FAB.
|
||||
// Land on the call view as soon as the call starts here, whether or not anyone's
|
||||
// camera is on — chat is one tap away via the control bar's chat toggle.
|
||||
if (!wasConnectedHere) {
|
||||
videoCallLayout.set(VideoCallLayout.Video)
|
||||
wasConnectedHere = true
|
||||
|
|
@ -499,52 +474,39 @@
|
|||
|
||||
<div
|
||||
class={cx(
|
||||
"flex min-h-0 flex-1 flex-col",
|
||||
isOverlayChat && "max-md:relative",
|
||||
// Desktop: chat is a real flex sibling beside the video pane rather than an
|
||||
// absolute overlay on top of it, so opening/closing it actually resizes the
|
||||
// video pane instead of just floating a card over it — see the sidebar's
|
||||
// width transition below for why this fixes controls jumping on toggle.
|
||||
voiceConnectedHere && "md:flex-row",
|
||||
"room flex min-h-0 min-w-0 flex-1 flex-col",
|
||||
voiceConnectedHere && "bg-surface xl:flex-row xl:overflow-hidden",
|
||||
)}>
|
||||
{#if h && voiceConnectedHere}
|
||||
<VideoCallContent
|
||||
layout={$videoCallLayout}
|
||||
{url}
|
||||
{h}
|
||||
class="hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex" />
|
||||
{/if}
|
||||
|
||||
{#if h && voiceConnectedHere}
|
||||
<VideoCallContent
|
||||
layout={$videoCallLayout}
|
||||
mobile
|
||||
{url}
|
||||
{h}
|
||||
class={cx("md:hidden", isOverlayChat && "absolute inset-0")} />
|
||||
<!-- The controls belong to the video, so they share its column and the chat panel
|
||||
runs the full height beside it. Below xl the open chat takes the pane over
|
||||
instead of sharing it, leaving this column to shrink to just the controls. -->
|
||||
<div
|
||||
class={cx(
|
||||
"flex min-h-0 min-w-0 flex-col max-xl:order-last",
|
||||
callChatOpen ? "xl:flex-1" : "flex-1",
|
||||
)}>
|
||||
<VideoCallContent {url} {h} class={cx(callChatOpen && "max-xl:hidden")} />
|
||||
<div class="hide-on-keyboard flex shrink-0 items-center justify-center pb-2">
|
||||
<CallControlBar {url} {h} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Closed at xl, the negative margin takes the panel out of the flex line without
|
||||
changing its width, parking it past the room's clipped right edge — so the
|
||||
transition slides it in at full size rather than squeezing its contents. It
|
||||
only drops out of the layout below xl, where it has nothing to share the pane
|
||||
with. inert (not just pointer-events) so a parked panel leaves the tab order. -->
|
||||
<div
|
||||
inert={callChatHidden}
|
||||
class={cx(
|
||||
"room flex min-h-0 min-w-0 flex-1 flex-col",
|
||||
videoCallChatHidden && "hidden",
|
||||
// Desktop: a real sidebar column (fixed width, animated) instead of an absolute
|
||||
// overlay card — its width transition is what makes the video pane (and the
|
||||
// controls centered inside it, see VideoCallContent) resize and re-center in
|
||||
// step with the chat opening/closing, instead of jumping.
|
||||
"relative flex min-h-0 min-w-0 flex-1 flex-col",
|
||||
voiceConnectedHere &&
|
||||
"md:flex md:min-w-0 md:flex-none md:border-0 md:border-l md:border-line md:bg-surface md:shadow-lg md:transition-all md:duration-300 md:ease-in-out",
|
||||
voiceConnectedHere &&
|
||||
(isOverlayChat
|
||||
? "md:w-72 md:opacity-100"
|
||||
: "md:w-0 md:border-l-0 md:opacity-0 md:shadow-none md:pointer-events-none"),
|
||||
isOverlayChat &&
|
||||
// Narrower width (w-72) plus generous insets keep the mobile card's edges from
|
||||
// landing flush against a video tile's edge. No shadow on mobile — the card
|
||||
// already reads as distinct from the video behind it via its own background.
|
||||
"max-md:absolute max-md:inset-x-2 max-md:top-4 max-md:bottom-[calc(3.5rem+var(--saib))] max-md:z-popover max-md:overflow-hidden max-md:rounded-2xl max-md:border max-md:border-line max-md:bg-surface/95",
|
||||
"xl:w-96 xl:flex-none xl:border-l xl:border-line xl:transition-[margin-right] xl:duration-200",
|
||||
callChatHidden && "max-xl:hidden xl:-mr-96",
|
||||
)}>
|
||||
{#if isOverlayChat}
|
||||
{#if callChatOpen}
|
||||
<Button
|
||||
aria-label="Close room chat"
|
||||
class="button button-neutral button-xs button-circle absolute right-2 top-2 z-popover"
|
||||
|
|
@ -553,26 +515,12 @@
|
|||
</Button>
|
||||
{/if}
|
||||
|
||||
<!-- Separate from the panel's own overflow: the panel itself can't clip its own
|
||||
overflow (that would also clip its desktop shadow), so this inner wrapper is
|
||||
what keeps content from spilling out while the panel's width is animating
|
||||
toward 0 on close. -->
|
||||
<div
|
||||
class={cx(
|
||||
"flex min-h-0 min-w-0 flex-1 flex-col",
|
||||
voiceConnectedHere && "md:overflow-hidden",
|
||||
)}>
|
||||
{#if h}
|
||||
<RoomPinnedMessages {url} {h} />
|
||||
{/if}
|
||||
{#if h}
|
||||
<RoomPinnedMessages {url} {h} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={element}
|
||||
onscroll={onScroll}
|
||||
class={cx(
|
||||
"room__content scroll-container",
|
||||
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
||||
)}>
|
||||
<div class="relative flex min-h-0 flex-1 flex-col">
|
||||
<div bind:this={element} onscroll={onScroll} class="room__content scroll-container">
|
||||
{#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="py-20">
|
||||
<div class="card flex flex-col gap-8 m-auto max-w-md items-center text-center">
|
||||
|
|
@ -643,106 +591,77 @@
|
|||
<div class="h-screen"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={cx(
|
||||
// no explicit flex-row: it's flex's default direction, and Tailwind emits
|
||||
// .flex-col before .flex-row in the compiled sheet, so having both classes
|
||||
// present at once (base flex-row + conditional flex-col below) let flex-row
|
||||
// always win — the compose row and call controls never actually stacked.
|
||||
"room__compose flex items-center gap-1 px-2",
|
||||
showMobileVideoPanel && "max-md:hidden",
|
||||
// the connected bar (mic/camera/screenshare/settings/chat/leave) is too wide
|
||||
// to sit beside the compose input on any screen size — give it its own row.
|
||||
// gap-2 only applies here (the inline row stays flush with no gap).
|
||||
voiceConnectedHere && "flex-col items-stretch gap-2",
|
||||
)}>
|
||||
<div class="room__compose-inner min-w-0 flex-1">
|
||||
{#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted}
|
||||
<!-- pass -->
|
||||
{:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Ask to Join
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#if parent}
|
||||
<RoomComposeParent {url} event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
{#if sharedEvent}
|
||||
<RoomComposeParent {url} event={sharedEvent} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<RoomComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key initialValues}
|
||||
<RoomCompose
|
||||
{url}
|
||||
{h}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
{initialValues}
|
||||
bind:this={compose} />
|
||||
{/key}
|
||||
{/if}
|
||||
{#if showScrollButton}
|
||||
<div in:fade class="absolute bottom-2 right-4 z-popover">
|
||||
<Button class="button button-neutral button-circle" onclick={scrollToBottom}>
|
||||
<Icon icon={AltArrowDown} />
|
||||
</Button>
|
||||
</div>
|
||||
{#if h}
|
||||
<div
|
||||
class={cx(
|
||||
"hide-on-keyboard flex flex-shrink-0 items-center justify-center py-2",
|
||||
// CallControlBar's connected pill is itself md:hidden here (a wider copy
|
||||
// floats over the video instead — see VideoCallContent.svelte), so once
|
||||
// this row actually stacks (flex-col, above) this wrapper has nothing to
|
||||
// show on desktop and was otherwise left as an empty padded bar under
|
||||
// the compose box. voiceConnectedHere is exactly the state where the
|
||||
// pill is connected, so hiding it here is safe for the join/joining
|
||||
// states, which don't hit this flex-col branch at all.
|
||||
voiceConnectedHere && "md:hidden",
|
||||
)}>
|
||||
<CallControlBar {url} {h} hideConnectedOnDesktop />
|
||||
{/if}
|
||||
|
||||
{#if showFixedNewMessages}
|
||||
<div
|
||||
transition:fly={{duration: 200}}
|
||||
class="absolute inset-x-0 top-2 z-popover flex justify-center">
|
||||
<Button class="button button-primary button-xs button-pill" onclick={scrollToNewMessages}>
|
||||
New Messages
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="room__compose flex items-center gap-1 px-2">
|
||||
<div class="room__compose-inner min-w-0 flex-1">
|
||||
{#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted}
|
||||
<!-- pass -->
|
||||
{:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}
|
||||
<div class="card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||
{#if $membershipStatus === MembershipStatus.Pending}
|
||||
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
|
||||
<Icon icon={ClockCircle} />
|
||||
Access Pending
|
||||
</Button>
|
||||
{:else}
|
||||
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
|
||||
{#if joining}
|
||||
<Spinner size="sm" />
|
||||
{:else}
|
||||
<Icon icon={Login2} />
|
||||
{/if}
|
||||
Ask to Join
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#if parent}
|
||||
<RoomComposeParent {url} event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
{#if sharedEvent}
|
||||
<RoomComposeParent {url} event={sharedEvent} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<RoomComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key initialValues}
|
||||
<RoomCompose
|
||||
{url}
|
||||
{h}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
{initialValues}
|
||||
bind:this={compose} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
{#if h && isVoiceRoom && !voiceConnectedHere}
|
||||
<div class="hide-on-keyboard flex shrink-0 items-center justify-center py-2">
|
||||
<CallControlBar {url} {h} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showScrollButton}
|
||||
<div
|
||||
in:fade
|
||||
class={cx(
|
||||
"chat__scroll-down",
|
||||
callBannerVisible && "chat__scroll-down--banner",
|
||||
isOverlayChat && "chat__scroll-down--overlay-chat",
|
||||
)}>
|
||||
<Button class="button button-neutral button-circle" onclick={scrollToBottom}>
|
||||
<Icon icon={AltArrowDown} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showFixedNewMessages}
|
||||
<div class="relative z-popover flex justify-center">
|
||||
<div transition:fly={{duration: 200}} class="fixed top-12 pt-sai">
|
||||
<Button class="button button-primary button-xs button-pill" onclick={scrollToNewMessages}>
|
||||
New Messages
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,8 @@
|
|||
import Icon from "@lib/components/Icon.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import VideoCallTile from "@app/components/VideoCallTile.svelte"
|
||||
import CallControlBar from "@app/components/CallControlBar.svelte"
|
||||
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
||||
import {
|
||||
VideoCallLayout,
|
||||
toggleVideoPrimaryTile,
|
||||
videoPrimaryTileKey,
|
||||
currentCallSession,
|
||||
|
|
@ -25,8 +23,6 @@
|
|||
import {deriveDisplaysByPubkey} from "@app/social"
|
||||
|
||||
type Props = {
|
||||
layout: VideoCallLayout
|
||||
mobile?: boolean
|
||||
url: string
|
||||
h: string
|
||||
class?: string
|
||||
|
|
@ -42,16 +38,7 @@
|
|||
|
||||
type TileLayoutVariant = "spotlight" | "default" | "strip"
|
||||
|
||||
const {layout, mobile = false, url, h, class: className = ""}: Props = $props()
|
||||
|
||||
const isViewingCurrentCallRoom = $derived(
|
||||
$callTargetRoom?.url === url && $callTargetRoom?.h === h,
|
||||
)
|
||||
|
||||
const showVideoContent = $derived(
|
||||
isViewingCurrentCallRoom &&
|
||||
(layout === VideoCallLayout.Split || layout === VideoCallLayout.Video),
|
||||
)
|
||||
const {url, h, class: className}: Props = $props()
|
||||
|
||||
const videoTiles = $derived.by(() => {
|
||||
const session = $currentCallSession
|
||||
|
|
@ -188,24 +175,6 @@
|
|||
const spotlightHandlerFor = (key: string) => () => {
|
||||
toggleVideoPrimaryTile(key)
|
||||
}
|
||||
|
||||
const panelChrome = $derived(
|
||||
mobile
|
||||
? cx(
|
||||
"flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden bg-surface px-2 pt-4 md:hidden pb-[calc(3.5rem+var(--saib))]",
|
||||
className,
|
||||
)
|
||||
: "flex min-h-0 w-full min-w-0 flex-1 flex-col gap-2 overflow-hidden bg-surface px-2 pb-2 pt-4",
|
||||
)
|
||||
|
||||
// Desktop: `className` (the instance's own visibility/sizing, e.g. "hidden ...
|
||||
// md:flex") lives on this wrapper rather than on panelChrome, so the floating
|
||||
// control bar below shares its box — a `relative` box that's already sized to
|
||||
// the real (chat-sidebar-aware) video pane width, since RoomChat now lays the
|
||||
// chat sidebar out as a flex sibling instead of an absolute overlay. Centering
|
||||
// the controls against this box (instead of the viewport) is what keeps them
|
||||
// centered in the actual remaining space as that width animates open/closed.
|
||||
const desktopWrapperClass = $derived(cx("relative flex min-h-0 flex-1 flex-col", className))
|
||||
</script>
|
||||
|
||||
{#snippet videoTile(tile: VideoTileData, layout: TileLayoutVariant)}
|
||||
|
|
@ -281,7 +250,7 @@
|
|||
<div
|
||||
bind:clientWidth={gridWidth}
|
||||
bind:clientHeight={gridHeight}
|
||||
class="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
||||
class="min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable_both-edges]">
|
||||
{#if tileGrid}
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
{#each tileGrid.rows as row, rowIndex (rowIndex)}
|
||||
|
|
@ -312,36 +281,6 @@
|
|||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if showVideoContent}
|
||||
{#if mobile}
|
||||
<div class={panelChrome}>
|
||||
{@render videoPanelBody()}
|
||||
</div>
|
||||
<!-- fixed (viewport-relative), not absolute within panelChrome — panelChrome's
|
||||
flex-computed height isn't guaranteed to reach the real viewport bottom, so
|
||||
an absolute child positioned against its box can end up overlapping the
|
||||
fixed bottom nav bar. Matches the same bottom-nav-clearing offset the old
|
||||
chat FAB used.
|
||||
|
||||
RoomChat mounts both a desktop and a mobile VideoCallContent instance at
|
||||
once while connected, each gated by its own `class` prop — this instance
|
||||
is the mobile one, so it's the only copy visible below md. -->
|
||||
<div
|
||||
class="left-content pointer-events-none fixed right-sai z-popover flex justify-center bottom-[calc(4.5rem+var(--saib))] md:hidden">
|
||||
<CallControlBar {url} {h} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- desktopWrapperClass carries this instance's own visibility ("hidden ...
|
||||
md:flex") and is `relative`, so the control bar below is `absolute` against
|
||||
the real (chat-sidebar-aware) video pane box rather than the viewport —
|
||||
it re-centers automatically as that box's width animates. -->
|
||||
<div class={desktopWrapperClass}>
|
||||
<div class={panelChrome}>
|
||||
{@render videoPanelBody()}
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-4 z-popover flex justify-center">
|
||||
<CallControlBar {url} {h} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class={cx("flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden p-2", className)}>
|
||||
{@render videoPanelBody()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ body.keyboard-open {
|
|||
body.keyboard-open .hide-on-keyboard {
|
||||
@apply hidden;
|
||||
}
|
||||
body.keyboard-open .room__compose {
|
||||
body.keyboard-open .room {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,4 @@
|
|||
@layer components {
|
||||
/* z-popover (not z-feature) so it stacks above the overlay chat pane (also
|
||||
z-popover) instead of landing behind it — same tier + later-in-DOM trick
|
||||
RoomChat's "New Messages" chip already relies on. */
|
||||
.chat__scroll-down {
|
||||
@apply z-popover fixed right-4 bottom-28 md:bottom-16;
|
||||
padding-bottom: var(--saib);
|
||||
}
|
||||
|
||||
/* .chat__scroll-down is `fixed` (viewport-relative), so it ignores CallBanner's real
|
||||
in-flow layout space entirely even though CallBanner shrinks the chat area above
|
||||
it. Nudge the button up by CallBanner's own approximate rendered height (~3.5rem:
|
||||
border-top + py-2 padding + its tallest child, the button-sm leave/mute controls)
|
||||
so it clears the banner instead of landing behind it. */
|
||||
.chat__scroll-down--banner {
|
||||
@apply bottom-[calc(7rem+3.5rem)] md:bottom-[calc(4rem+3.5rem)];
|
||||
}
|
||||
|
||||
/* On desktop, isOverlayChat (Split layout) floats the chat pane at md:bottom-24
|
||||
in this same corner — .chat__scroll-down's default md:bottom-16 puts the
|
||||
button's top edge past that pane's bottom edge, clipping into its rounded
|
||||
corner. Drop below it instead. Mobile is untouched: there the pane fills
|
||||
nearly the whole screen, so the button is meant to float over it. */
|
||||
.chat__scroll-down--overlay-chat {
|
||||
@apply md:bottom-10;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
@apply bg-surface-less text-content mx-1 mb-2 flex min-w-[100px] cursor-auto flex-col gap-1 rounded-2xl p-2 text-left lg:max-w-2xl;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
@layer components {
|
||||
.room {
|
||||
@apply relative;
|
||||
/* The mobile nav is fixed on top of the page, so the room view reserves its
|
||||
height here rather than in each of its children — otherwise an overlaid or
|
||||
floating child ends up stacking a second copy of the offset. */
|
||||
@apply relative mb-14 md:mb-0;
|
||||
|
||||
.room__content {
|
||||
@apply z-feature flex max-h-screen min-h-0 w-full min-w-0 flex-col flex-col-reverse gap-2 overflow-x-hidden overflow-y-auto;
|
||||
|
|
@ -19,7 +22,7 @@
|
|||
}
|
||||
|
||||
.room__compose {
|
||||
@apply z-compose relative mb-14 shrink-0 md:mb-0;
|
||||
@apply z-compose relative shrink-0;
|
||||
}
|
||||
|
||||
.room__compose .room__compose-inner {
|
||||
|
|
|
|||
Loading…
Reference in a new issue