From 2974faa2e94984b088510be06ae24ff0d2f4b381 Mon Sep 17 00:00:00 2001 From: Aditya Chaudhary <30+useradityaa@noreply.coracle.social> Date: Thu, 27 Aug 2026 23:06:10 +0000 Subject: [PATCH] fix: voice room UI overlap, sizing, and scroll button issues (#371) Co-authored-by: Aditya Chaudhary <30+useradityaa@noreply.coracle.social> --- src/app/call.ts | 9 + src/app/components/CallBanner.svelte | 16 +- src/app/components/CallControlBar.svelte | 43 ++- src/app/components/RoomChat.svelte | 332 ++++++++++++--------- src/app/components/VideoCallContent.svelte | 72 +++-- src/lib/components/chat.css | 23 +- 6 files changed, 305 insertions(+), 190 deletions(-) diff --git a/src/app/call.ts b/src/app/call.ts index 8e3e4752..bfa3cd81 100644 --- a/src/app/call.ts +++ b/src/app/call.ts @@ -286,6 +286,15 @@ export const callState = writable(CallState.Disconnected) export const callTargetRoom = writable(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([]) export const participantMediaState = writable(new Map()) diff --git a/src/app/components/CallBanner.svelte b/src/app/components/CallBanner.svelte index 856b7ac8..aaff2230 100644 --- a/src/app/components/CallBanner.svelte +++ b/src/app/components/CallBanner.svelte @@ -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) : "", diff --git a/src/app/components/CallControlBar.svelte b/src/app/components/CallControlBar.svelte index df654cb2..63421aad 100644 --- a/src/app/components/CallControlBar.svelte +++ b/src/app/components/CallControlBar.svelte @@ -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 @@ {/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", )}> {#if $currentCallSession} {#if !Capacitor.isNativePlatform()} {/if} {/if} {/if} diff --git a/src/app/components/RoomChat.svelte b/src/app/components/RoomChat.svelte index 06485ed6..62269091 100644 --- a/src/app/components/RoomChat.svelte +++ b/src/app/components/RoomChat.svelte @@ -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 @@ }) -
+
{#if h && voiceConnectedHere} + 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} - {:else} - - {/if} -
-
- {:else} - {#if loadingForward && elements.length > 0} -

- Looking for messages... -

- {/if} - {#each elements as { type, id, value, showPubkey } (id)} - {#if type === "new-messages"} -
-
-

- New Messages -

-
-
- {:else if type === "date"} - {value} - {:else} - {@const event = value as TrustedEvent} - {#if event.kind === addMemberKind} - - {:else} - - {/if} - {/if} - {/each} -

- {#if loadingBackward} - Looking for messages... - {:else} - End of message history - {/if} -

+ {#if h} + {/if} -
- -
-
+
{#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted} - - {:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted} -
-

Only members are allowed to post to this room.

- {#if $membershipStatus === MembershipStatus.Pending} - - {:else} - - {/if} +
+
+

You aren't currently a member of this room.

+ {#if $membershipStatus === MembershipStatus.Pending} + + {:else} + + {/if} +
{:else} -
- {#if parent} - + {#if loadingForward && elements.length > 0} +

+ Looking for messages... +

+ {/if} + {#each elements as { type, id, value, showPubkey } (id)} + {#if type === "new-messages"} +
+
+

+ New Messages +

+
+
+ {:else if type === "date"} + {value} + {:else} + {@const event = value as TrustedEvent} + {#if event.kind === addMemberKind} + + {:else} + + {/if} {/if} - {#if sharedEvent} - - {/if} - {#if eventToEdit} - + {/each} +

+ {#if loadingBackward} + Looking for messages... + {:else} + End of message history {/if} +

+ {/if} +
+
+ +
+
+ {#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted} + + {:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted} +
+

Only members are allowed to post to this room.

+ {#if $membershipStatus === MembershipStatus.Pending} + + {:else} + + {/if} +
+ {:else} +
+ {#if parent} + + {/if} + {#if sharedEvent} + + {/if} + {#if eventToEdit} + + {/if} +
+ {#key initialValues} + + {/key} + {/if} +
+ {#if h} +
+
- {#key initialValues} - - {/key} {/if}
- {#if h} -
- -
- {/if}
{#if showScrollButton} -
+
diff --git a/src/app/components/VideoCallContent.svelte b/src/app/components/VideoCallContent.svelte index 4443d997..85291466 100644 --- a/src/app/components/VideoCallContent.svelte +++ b/src/app/components/VideoCallContent.svelte @@ -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)) {#snippet videoTile(tile: VideoTileData, layout: TileLayoutVariant)} @@ -305,25 +313,35 @@ {/snippet} {#if showVideoContent} -
- {@render videoPanelBody()} -
- -
- -
+ 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. --> +
+ +
+ {:else} + +
+
+ {@render videoPanelBody()} +
+
+ +
+
+ {/if} {/if} diff --git a/src/lib/components/chat.css b/src/lib/components/chat.css index 0d08325a..afeab558 100644 --- a/src/lib/components/chat.css +++ b/src/lib/components/chat.css @@ -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; }