fix: voice room UI overlap, sizing, and scroll button issues (#371)
Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social>
This commit is contained in:
parent
fe9b0fe87a
commit
2974faa2e9
6 changed files with 305 additions and 190 deletions
|
|
@ -286,6 +286,15 @@ export const callState = writable<CallState>(CallState.Disconnected)
|
|||
|
||||
export const callTargetRoom = writable<Room | undefined>(undefined)
|
||||
|
||||
export const deriveIsCallActiveElsewhere = (url: string | undefined, h: string | undefined) =>
|
||||
derived(
|
||||
[callState, callTargetRoom],
|
||||
([$state, $targetRoom]) =>
|
||||
($state === CallState.Joining || $state === CallState.Connected) &&
|
||||
$targetRoom !== undefined &&
|
||||
!($targetRoom.url === url && $targetRoom.h === h),
|
||||
)
|
||||
|
||||
export const speakingParticipants = writable<CallParticipant[]>([])
|
||||
|
||||
export const participantMediaState = writable(new Map<string, ParticipantMediaState>())
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
callTargetRoom,
|
||||
callMicMuted,
|
||||
cancelJoinVoiceRoom,
|
||||
deriveIsCallActiveElsewhere,
|
||||
leaveVoiceRoom,
|
||||
toggleMute,
|
||||
} from "@app/call"
|
||||
|
|
@ -27,19 +28,10 @@
|
|||
|
||||
// The call's own room page already shows full controls (CallControlBar), so
|
||||
// the banner would just be redundant clutter there.
|
||||
const isViewingCurrentVoiceRoom = $derived(
|
||||
$callTargetRoom !== undefined &&
|
||||
routeUrl !== undefined &&
|
||||
typeof h === "string" &&
|
||||
$callTargetRoom.url === routeUrl &&
|
||||
$callTargetRoom.h === h,
|
||||
)
|
||||
|
||||
const visible = $derived(
|
||||
($callState === CallState.Joining || $callState === CallState.Connected) &&
|
||||
$callTargetRoom !== undefined &&
|
||||
!isViewingCurrentVoiceRoom,
|
||||
const isCallActiveElsewhere = $derived(
|
||||
deriveIsCallActiveElsewhere(routeUrl, typeof h === "string" ? h : undefined),
|
||||
)
|
||||
const visible = $derived($isCallActiveElsewhere)
|
||||
|
||||
const roomName = $derived(
|
||||
$callTargetRoom ? displayRoom($callTargetRoom.url, $callTargetRoom.h) : "",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,16 @@
|
|||
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 onChatToggle = () => {
|
||||
videoCallLayout.update(p =>
|
||||
p === VideoCallLayout.Split ? VideoCallLayout.Video : VideoCallLayout.Split,
|
||||
|
|
@ -93,9 +103,9 @@
|
|||
<Button
|
||||
data-tip="Cancel"
|
||||
aria-label="Cancel joining voice room"
|
||||
class="button button-circle button-xs button-neutral"
|
||||
class="button button-circle button-sm button-neutral"
|
||||
onclick={cancelJoinVoiceRoom}>
|
||||
<Icon icon={CloseCircle} size={3} />
|
||||
<Icon icon={CloseCircle} size={4} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -105,6 +115,7 @@
|
|||
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",
|
||||
)}>
|
||||
<Button
|
||||
|
|
@ -112,11 +123,12 @@
|
|||
aria-label={$callMicMuted ? "Unmute microphone" : "Mute microphone"}
|
||||
aria-pressed={!$callMicMuted}
|
||||
class={cx(
|
||||
"button button-circle max-md:h-9 max-md:w-9",
|
||||
"button button-circle",
|
||||
compactButtonClass,
|
||||
$callMicMuted ? "button-neutral" : "button-primary",
|
||||
)}
|
||||
onclick={toggleMute}>
|
||||
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={4.5} />
|
||||
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={iconSize} />
|
||||
</Button>
|
||||
{#if $currentCallSession}
|
||||
<Button
|
||||
|
|
@ -124,13 +136,14 @@
|
|||
aria-label={$currentCallSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
||||
aria-pressed={$currentCallSession.cameraOn}
|
||||
class={cx(
|
||||
"button button-circle max-md:h-9 max-md:w-9",
|
||||
"button button-circle",
|
||||
compactButtonClass,
|
||||
$currentCallSession.cameraOn ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={toggleCamera}>
|
||||
<Icon
|
||||
icon={$currentCallSession.cameraOn ? VideocameraRecord : VideocameraOff}
|
||||
size={4.5} />
|
||||
size={iconSize} />
|
||||
</Button>
|
||||
{#if !Capacitor.isNativePlatform()}
|
||||
<Button
|
||||
|
|
@ -140,31 +153,33 @@
|
|||
: "Share screen"}
|
||||
aria-pressed={$currentCallSession.screenShareOn}
|
||||
class={cx(
|
||||
"button button-circle max-md:h-9 max-md:w-9",
|
||||
"button button-circle",
|
||||
compactButtonClass,
|
||||
$currentCallSession.screenShareOn ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={toggleScreenShare}>
|
||||
<Icon icon={Monitor} size={4.5} />
|
||||
<Icon icon={Monitor} size={iconSize} />
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
data-tip="Call settings"
|
||||
aria-label="Call settings"
|
||||
class="button button-circle button-neutral max-md:h-9 max-md:w-9"
|
||||
class={cx("button button-circle button-neutral", compactButtonClass)}
|
||||
onclick={openCallSettings}>
|
||||
<Icon icon={Settings} size={4.5} />
|
||||
<Icon icon={Settings} size={iconSize} />
|
||||
</Button>
|
||||
<Button
|
||||
data-tip="Toggle chat"
|
||||
aria-label="Toggle chat panel"
|
||||
aria-pressed={isChatPanelActive}
|
||||
class={cx(
|
||||
"button button-circle relative max-md:h-9 max-md:w-9",
|
||||
"button button-circle relative",
|
||||
compactButtonClass,
|
||||
isChatPanelActive ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={onChatToggle}>
|
||||
<Icon icon={ChatRound} size={4.5} />
|
||||
<Icon icon={ChatRound} size={iconSize} />
|
||||
{#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"
|
||||
|
|
@ -175,9 +190,9 @@
|
|||
<Button
|
||||
data-tip="Leave call"
|
||||
aria-label="Leave voice room"
|
||||
class="button button-circle button-error max-md:h-9 max-md:w-9"
|
||||
class={cx("button button-circle button-error", compactButtonClass)}
|
||||
onclick={leaveVoiceRoom}>
|
||||
<Icon icon={EndCall} size={5} />
|
||||
<Icon icon={EndCall} size={isChatPanelActive ? 4 : 5} />
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,14 @@
|
|||
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} from "@app/call"
|
||||
import {
|
||||
CallState,
|
||||
callTargetRoom,
|
||||
callState,
|
||||
VideoCallLayout,
|
||||
videoCallLayout,
|
||||
deriveIsCallActiveElsewhere,
|
||||
} from "@app/call"
|
||||
import {
|
||||
PROTECTED,
|
||||
RoomType,
|
||||
|
|
@ -64,6 +71,9 @@
|
|||
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.
|
||||
|
|
@ -487,16 +497,22 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class={cx("flex min-h-0 flex-1 flex-col", isOverlayChat && "relative")}>
|
||||
<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",
|
||||
)}>
|
||||
{#if h && voiceConnectedHere}
|
||||
<VideoCallContent
|
||||
layout={$videoCallLayout}
|
||||
{url}
|
||||
{h}
|
||||
class={cx(
|
||||
"hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex",
|
||||
isOverlayChat && "md:absolute md:inset-0",
|
||||
)} />
|
||||
class="hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex" />
|
||||
{/if}
|
||||
|
||||
{#if h && voiceConnectedHere}
|
||||
|
|
@ -512,8 +528,21 @@
|
|||
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.
|
||||
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 &&
|
||||
"absolute inset-x-2 top-4 bottom-[calc(3.5rem+var(--saib))] z-popover overflow-hidden rounded-2xl border border-line bg-surface/95 shadow-lg md:left-auto md:right-4 md:bottom-4 md:w-96 md:max-w-[26rem] md:bg-surface/90",
|
||||
// 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",
|
||||
)}>
|
||||
{#if isOverlayChat}
|
||||
<Button
|
||||
|
|
@ -524,153 +553,184 @@
|
|||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if h}
|
||||
<RoomPinnedMessages {url} {h} />
|
||||
{/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
|
||||
bind:this={element}
|
||||
onscroll={onScroll}
|
||||
class={cx(
|
||||
"room__content scroll-container",
|
||||
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
||||
"flex min-h-0 min-w-0 flex-1 flex-col",
|
||||
voiceConnectedHere && "md:overflow-hidden",
|
||||
)}>
|
||||
{#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">
|
||||
<p class="opacity-75">You aren't currently a member of 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}
|
||||
Join Room
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if loadingForward && elements.length > 0}
|
||||
<p class="py-20 flex justify-center">
|
||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||
</p>
|
||||
{/if}
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "new-messages"}
|
||||
<div
|
||||
{id}
|
||||
class={cx("flex items-center py-2 text-xs transition-colors", {
|
||||
"opacity-0": showFixedNewMessages,
|
||||
})}>
|
||||
<div class="h-px grow bg-primary text-primary-content"></div>
|
||||
<p
|
||||
class="rounded-full bg-primary text-primary-content px-2 py-1"
|
||||
style="color: var(--primary-content)">
|
||||
New Messages
|
||||
</p>
|
||||
<div class="h-px grow bg-primary text-primary-content"></div>
|
||||
</div>
|
||||
{:else if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else}
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === addMemberKind}
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else}
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
{showPubkey}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
{#if loadingBackward}
|
||||
<Spinner loading={loadingBackward}>Looking for messages...</Spinner>
|
||||
{:else}
|
||||
<Spinner>End of message history</Spinner>
|
||||
{/if}
|
||||
</p>
|
||||
{#if h}
|
||||
<RoomPinnedMessages {url} {h} />
|
||||
{/if}
|
||||
<div class="h-screen"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={cx(
|
||||
"room__compose flex flex-row 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">
|
||||
<div
|
||||
bind:this={element}
|
||||
onscroll={onScroll}
|
||||
class={cx(
|
||||
"room__content scroll-container",
|
||||
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
||||
)}>
|
||||
{#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 class="py-20">
|
||||
<div class="card flex flex-col gap-8 m-auto max-w-md items-center text-center">
|
||||
<p class="opacity-75">You aren't currently a member of 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}
|
||||
Join Room
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#if parent}
|
||||
<RoomComposeParent {url} event={parent} clear={clearParent} verb="Replying to" />
|
||||
{#if loadingForward && elements.length > 0}
|
||||
<p class="py-20 flex justify-center">
|
||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||
</p>
|
||||
{/if}
|
||||
{#each elements as { type, id, value, showPubkey } (id)}
|
||||
{#if type === "new-messages"}
|
||||
<div
|
||||
{id}
|
||||
class={cx("flex items-center py-2 text-xs transition-colors", {
|
||||
"opacity-0": showFixedNewMessages,
|
||||
})}>
|
||||
<div class="h-px grow bg-primary text-primary-content"></div>
|
||||
<p
|
||||
class="rounded-full bg-primary text-primary-content px-2 py-1"
|
||||
style="color: var(--primary-content)">
|
||||
New Messages
|
||||
</p>
|
||||
<div class="h-px grow bg-primary text-primary-content"></div>
|
||||
</div>
|
||||
{:else if type === "date"}
|
||||
<Divider>{value}</Divider>
|
||||
{:else}
|
||||
{@const event = value as TrustedEvent}
|
||||
{#if event.kind === addMemberKind}
|
||||
<RoomItemAddMember {url} {event} />
|
||||
{:else}
|
||||
<RoomItem
|
||||
{url}
|
||||
{event}
|
||||
{replyTo}
|
||||
{showPubkey}
|
||||
canEdit={canEditEvent}
|
||||
onEdit={onEditEvent} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if sharedEvent}
|
||||
<RoomComposeParent {url} event={sharedEvent} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<RoomComposeEdit clear={clearEventToEdit} />
|
||||
{/each}
|
||||
<p class="flex h-10 items-center justify-center py-20">
|
||||
{#if loadingBackward}
|
||||
<Spinner loading={loadingBackward}>Looking for messages...</Spinner>
|
||||
{:else}
|
||||
<Spinner>End of message history</Spinner>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
<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}
|
||||
</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 />
|
||||
</div>
|
||||
{#key initialValues}
|
||||
<RoomCompose
|
||||
{url}
|
||||
{h}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
{initialValues}
|
||||
bind:this={compose} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
{#if h}
|
||||
<div class="hide-on-keyboard flex flex-shrink-0 items-center justify-center py-2">
|
||||
<CallControlBar {url} {h} hideConnectedOnDesktop />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showScrollButton}
|
||||
<div in:fade class="chat__scroll-down">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -190,14 +190,22 @@
|
|||
}
|
||||
|
||||
const panelChrome = $derived(
|
||||
cx(
|
||||
mobile &&
|
||||
"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))]",
|
||||
!mobile &&
|
||||
"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",
|
||||
className,
|
||||
),
|
||||
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)}
|
||||
|
|
@ -305,25 +313,35 @@
|
|||
{/snippet}
|
||||
|
||||
{#if showVideoContent}
|
||||
<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.
|
||||
{#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, e.g. "md:hidden"
|
||||
vs "hidden ... md:flex"), so this fixed bar needs the same mobile/desktop
|
||||
split — otherwise both instances render their own copy at the identical
|
||||
screen position simultaneously. -->
|
||||
<div
|
||||
class={cx(
|
||||
"left-content pointer-events-none fixed right-sai z-popover justify-center bottom-[calc(4.5rem+var(--saib))] md:bottom-4",
|
||||
mobile ? "flex md:hidden" : "hidden md:flex",
|
||||
)}>
|
||||
<CallControlBar {url} {h} />
|
||||
</div>
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,30 @@
|
|||
@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-feature fixed right-4 bottom-28 md:bottom-16;
|
||||
@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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue