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 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 speakingParticipants = writable<CallParticipant[]>([])
|
||||||
|
|
||||||
export const participantMediaState = writable(new Map<string, ParticipantMediaState>())
|
export const participantMediaState = writable(new Map<string, ParticipantMediaState>())
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
callTargetRoom,
|
callTargetRoom,
|
||||||
callMicMuted,
|
callMicMuted,
|
||||||
cancelJoinVoiceRoom,
|
cancelJoinVoiceRoom,
|
||||||
|
deriveIsCallActiveElsewhere,
|
||||||
leaveVoiceRoom,
|
leaveVoiceRoom,
|
||||||
toggleMute,
|
toggleMute,
|
||||||
} from "@app/call"
|
} from "@app/call"
|
||||||
|
|
@ -27,19 +28,10 @@
|
||||||
|
|
||||||
// The call's own room page already shows full controls (CallControlBar), so
|
// The call's own room page already shows full controls (CallControlBar), so
|
||||||
// the banner would just be redundant clutter there.
|
// the banner would just be redundant clutter there.
|
||||||
const isViewingCurrentVoiceRoom = $derived(
|
const isCallActiveElsewhere = $derived(
|
||||||
$callTargetRoom !== undefined &&
|
deriveIsCallActiveElsewhere(routeUrl, typeof h === "string" ? h : 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 visible = $derived($isCallActiveElsewhere)
|
||||||
|
|
||||||
const roomName = $derived(
|
const roomName = $derived(
|
||||||
$callTargetRoom ? displayRoom($callTargetRoom.url, $callTargetRoom.h) : "",
|
$callTargetRoom ? displayRoom($callTargetRoom.url, $callTargetRoom.h) : "",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,16 @@
|
||||||
const chatUnread = $derived($notifications.has(roomPath))
|
const chatUnread = $derived($notifications.has(roomPath))
|
||||||
const isChatPanelActive = $derived($videoCallLayout === VideoCallLayout.Split)
|
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 = () => {
|
const onChatToggle = () => {
|
||||||
videoCallLayout.update(p =>
|
videoCallLayout.update(p =>
|
||||||
p === VideoCallLayout.Split ? VideoCallLayout.Video : VideoCallLayout.Split,
|
p === VideoCallLayout.Split ? VideoCallLayout.Video : VideoCallLayout.Split,
|
||||||
|
|
@ -93,9 +103,9 @@
|
||||||
<Button
|
<Button
|
||||||
data-tip="Cancel"
|
data-tip="Cancel"
|
||||||
aria-label="Cancel joining voice room"
|
aria-label="Cancel joining voice room"
|
||||||
class="button button-circle button-xs button-neutral"
|
class="button button-circle button-sm button-neutral"
|
||||||
onclick={cancelJoinVoiceRoom}>
|
onclick={cancelJoinVoiceRoom}>
|
||||||
<Icon icon={CloseCircle} size={3} />
|
<Icon icon={CloseCircle} size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -105,6 +115,7 @@
|
||||||
out:fade={{duration: 120}}
|
out:fade={{duration: 120}}
|
||||||
class={cx(
|
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",
|
"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",
|
hideConnectedOnDesktop && "md:hidden",
|
||||||
)}>
|
)}>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -112,11 +123,12 @@
|
||||||
aria-label={$callMicMuted ? "Unmute microphone" : "Mute microphone"}
|
aria-label={$callMicMuted ? "Unmute microphone" : "Mute microphone"}
|
||||||
aria-pressed={!$callMicMuted}
|
aria-pressed={!$callMicMuted}
|
||||||
class={cx(
|
class={cx(
|
||||||
"button button-circle max-md:h-9 max-md:w-9",
|
"button button-circle",
|
||||||
|
compactButtonClass,
|
||||||
$callMicMuted ? "button-neutral" : "button-primary",
|
$callMicMuted ? "button-neutral" : "button-primary",
|
||||||
)}
|
)}
|
||||||
onclick={toggleMute}>
|
onclick={toggleMute}>
|
||||||
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={4.5} />
|
<Icon icon={$callMicMuted ? MicrophoneOff : Microphone} size={iconSize} />
|
||||||
</Button>
|
</Button>
|
||||||
{#if $currentCallSession}
|
{#if $currentCallSession}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -124,13 +136,14 @@
|
||||||
aria-label={$currentCallSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
aria-label={$currentCallSession.cameraOn ? "Turn off camera" : "Turn on camera"}
|
||||||
aria-pressed={$currentCallSession.cameraOn}
|
aria-pressed={$currentCallSession.cameraOn}
|
||||||
class={cx(
|
class={cx(
|
||||||
"button button-circle max-md:h-9 max-md:w-9",
|
"button button-circle",
|
||||||
|
compactButtonClass,
|
||||||
$currentCallSession.cameraOn ? "button-primary" : "button-neutral",
|
$currentCallSession.cameraOn ? "button-primary" : "button-neutral",
|
||||||
)}
|
)}
|
||||||
onclick={toggleCamera}>
|
onclick={toggleCamera}>
|
||||||
<Icon
|
<Icon
|
||||||
icon={$currentCallSession.cameraOn ? VideocameraRecord : VideocameraOff}
|
icon={$currentCallSession.cameraOn ? VideocameraRecord : VideocameraOff}
|
||||||
size={4.5} />
|
size={iconSize} />
|
||||||
</Button>
|
</Button>
|
||||||
{#if !Capacitor.isNativePlatform()}
|
{#if !Capacitor.isNativePlatform()}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -140,31 +153,33 @@
|
||||||
: "Share screen"}
|
: "Share screen"}
|
||||||
aria-pressed={$currentCallSession.screenShareOn}
|
aria-pressed={$currentCallSession.screenShareOn}
|
||||||
class={cx(
|
class={cx(
|
||||||
"button button-circle max-md:h-9 max-md:w-9",
|
"button button-circle",
|
||||||
|
compactButtonClass,
|
||||||
$currentCallSession.screenShareOn ? "button-primary" : "button-neutral",
|
$currentCallSession.screenShareOn ? "button-primary" : "button-neutral",
|
||||||
)}
|
)}
|
||||||
onclick={toggleScreenShare}>
|
onclick={toggleScreenShare}>
|
||||||
<Icon icon={Monitor} size={4.5} />
|
<Icon icon={Monitor} size={iconSize} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
data-tip="Call settings"
|
data-tip="Call settings"
|
||||||
aria-label="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}>
|
onclick={openCallSettings}>
|
||||||
<Icon icon={Settings} size={4.5} />
|
<Icon icon={Settings} size={iconSize} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-tip="Toggle chat"
|
data-tip="Toggle chat"
|
||||||
aria-label="Toggle chat panel"
|
aria-label="Toggle chat panel"
|
||||||
aria-pressed={isChatPanelActive}
|
aria-pressed={isChatPanelActive}
|
||||||
class={cx(
|
class={cx(
|
||||||
"button button-circle relative max-md:h-9 max-md:w-9",
|
"button button-circle relative",
|
||||||
|
compactButtonClass,
|
||||||
isChatPanelActive ? "button-primary" : "button-neutral",
|
isChatPanelActive ? "button-primary" : "button-neutral",
|
||||||
)}
|
)}
|
||||||
onclick={onChatToggle}>
|
onclick={onChatToggle}>
|
||||||
<Icon icon={ChatRound} size={4.5} />
|
<Icon icon={ChatRound} size={iconSize} />
|
||||||
{#if chatUnread}
|
{#if chatUnread}
|
||||||
<span
|
<span
|
||||||
class="absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full bg-primary ring-2 ring-surface"
|
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
|
<Button
|
||||||
data-tip="Leave call"
|
data-tip="Leave call"
|
||||||
aria-label="Leave voice room"
|
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}>
|
onclick={leaveVoiceRoom}>
|
||||||
<Icon icon={EndCall} size={5} />
|
<Icon icon={EndCall} size={isChatPanelActive ? 4 : 5} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,14 @@
|
||||||
import CallControlBar from "@app/components/CallControlBar.svelte"
|
import CallControlBar from "@app/components/CallControlBar.svelte"
|
||||||
import {deletes, relays, rooms, thunks, user} from "@app/core"
|
import {deletes, relays, rooms, thunks, user} from "@app/core"
|
||||||
import {joinRoom, leaveRoom} from "@app/access"
|
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 {
|
import {
|
||||||
PROTECTED,
|
PROTECTED,
|
||||||
RoomType,
|
RoomType,
|
||||||
|
|
@ -64,6 +71,9 @@
|
||||||
isVoiceRoom && $callState === CallState.Connected && isCallTargetingThisRoom,
|
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
|
// 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
|
// 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.
|
// — that previously hid this room's messages/compose row while connected elsewhere.
|
||||||
|
|
@ -487,16 +497,22 @@
|
||||||
})
|
})
|
||||||
</script>
|
</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}
|
{#if h && voiceConnectedHere}
|
||||||
<VideoCallContent
|
<VideoCallContent
|
||||||
layout={$videoCallLayout}
|
layout={$videoCallLayout}
|
||||||
{url}
|
{url}
|
||||||
{h}
|
{h}
|
||||||
class={cx(
|
class="hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex" />
|
||||||
"hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex",
|
|
||||||
isOverlayChat && "md:absolute md:inset-0",
|
|
||||||
)} />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if h && voiceConnectedHere}
|
{#if h && voiceConnectedHere}
|
||||||
|
|
@ -512,8 +528,21 @@
|
||||||
class={cx(
|
class={cx(
|
||||||
"room flex min-h-0 min-w-0 flex-1 flex-col",
|
"room flex min-h-0 min-w-0 flex-1 flex-col",
|
||||||
videoCallChatHidden && "hidden",
|
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 &&
|
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}
|
{#if isOverlayChat}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -524,153 +553,184 @@
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if h}
|
<!-- Separate from the panel's own overflow: the panel itself can't clip its own
|
||||||
<RoomPinnedMessages {url} {h} />
|
overflow (that would also clip its desktop shadow), so this inner wrapper is
|
||||||
{/if}
|
what keeps content from spilling out while the panel's width is animating
|
||||||
|
toward 0 on close. -->
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
|
||||||
onscroll={onScroll}
|
|
||||||
class={cx(
|
class={cx(
|
||||||
"room__content scroll-container",
|
"flex min-h-0 min-w-0 flex-1 flex-col",
|
||||||
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
voiceConnectedHere && "md:overflow-hidden",
|
||||||
)}>
|
)}>
|
||||||
{#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted}
|
{#if h}
|
||||||
<div class="py-20">
|
<RoomPinnedMessages {url} {h} />
|
||||||
<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}
|
{/if}
|
||||||
<div class="h-screen"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cx(
|
bind:this={element}
|
||||||
"room__compose flex flex-row items-center gap-1 px-2",
|
onscroll={onScroll}
|
||||||
showMobileVideoPanel && "max-md:hidden",
|
class={cx(
|
||||||
// the connected bar (mic/camera/screenshare/settings/chat/leave) is too wide
|
"room__content scroll-container",
|
||||||
// to sit beside the compose input on any screen size — give it its own row.
|
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
||||||
// 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}
|
{#if $room?.meta?.isPrivate() && $membershipStatus !== MembershipStatus.Granted}
|
||||||
<!-- pass -->
|
<div class="py-20">
|
||||||
{:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}
|
<div class="card flex flex-col gap-8 m-auto max-w-md items-center text-center">
|
||||||
<div class="card m-4 flex flex-row items-center justify-between px-4 py-3">
|
<p class="opacity-75">You aren't currently a member of this room.</p>
|
||||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
{#if $membershipStatus === MembershipStatus.Pending}
|
||||||
{#if $membershipStatus === MembershipStatus.Pending}
|
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
|
||||||
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
|
<Icon icon={ClockCircle} />
|
||||||
<Icon icon={ClockCircle} />
|
Access Pending
|
||||||
Access Pending
|
</Button>
|
||||||
</Button>
|
{:else}
|
||||||
{:else}
|
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
|
||||||
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
|
{#if joining}
|
||||||
{#if joining}
|
<Spinner size="sm" />
|
||||||
<Spinner size="sm" />
|
{:else}
|
||||||
{:else}
|
<Icon icon={Login2} />
|
||||||
<Icon icon={Login2} />
|
{/if}
|
||||||
{/if}
|
Join Room
|
||||||
Ask to Join
|
</Button>
|
||||||
</Button>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
{#if loadingForward && elements.length > 0}
|
||||||
{#if parent}
|
<p class="py-20 flex justify-center">
|
||||||
<RoomComposeParent {url} event={parent} clear={clearParent} verb="Replying to" />
|
<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}
|
||||||
{#if sharedEvent}
|
{/each}
|
||||||
<RoomComposeParent {url} event={sharedEvent} clear={clearShare} verb="Sharing" />
|
<p class="flex h-10 items-center justify-center py-20">
|
||||||
{/if}
|
{#if loadingBackward}
|
||||||
{#if eventToEdit}
|
<Spinner loading={loadingBackward}>Looking for messages...</Spinner>
|
||||||
<RoomComposeEdit clear={clearEventToEdit} />
|
{:else}
|
||||||
|
<Spinner>End of message history</Spinner>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
{#key initialValues}
|
|
||||||
<RoomCompose
|
|
||||||
{url}
|
|
||||||
{h}
|
|
||||||
{onSubmit}
|
|
||||||
{onEscape}
|
|
||||||
{onEditPrevious}
|
|
||||||
{initialValues}
|
|
||||||
bind:this={compose} />
|
|
||||||
{/key}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showScrollButton}
|
{#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}>
|
<Button class="button button-neutral button-circle" onclick={scrollToBottom}>
|
||||||
<Icon icon={AltArrowDown} />
|
<Icon icon={AltArrowDown} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -190,14 +190,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelChrome = $derived(
|
const panelChrome = $derived(
|
||||||
cx(
|
mobile
|
||||||
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))]",
|
"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 &&
|
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",
|
)
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
{#snippet videoTile(tile: VideoTileData, layout: TileLayoutVariant)}
|
{#snippet videoTile(tile: VideoTileData, layout: TileLayoutVariant)}
|
||||||
|
|
@ -305,25 +313,35 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#if showVideoContent}
|
{#if showVideoContent}
|
||||||
<div class={panelChrome}>
|
{#if mobile}
|
||||||
{@render videoPanelBody()}
|
<div class={panelChrome}>
|
||||||
</div>
|
{@render videoPanelBody()}
|
||||||
<!-- fixed (viewport-relative), not absolute within panelChrome — panelChrome's
|
</div>
|
||||||
flex-computed height isn't guaranteed to reach the real viewport bottom, so
|
<!-- fixed (viewport-relative), not absolute within panelChrome — panelChrome's
|
||||||
an absolute child positioned against its box can end up overlapping the
|
flex-computed height isn't guaranteed to reach the real viewport bottom, so
|
||||||
fixed bottom nav bar. Matches the same bottom-nav-clearing offset the old
|
an absolute child positioned against its box can end up overlapping the
|
||||||
chat FAB used.
|
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
|
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"
|
once while connected, each gated by its own `class` prop — this instance
|
||||||
vs "hidden ... md:flex"), so this fixed bar needs the same mobile/desktop
|
is the mobile one, so it's the only copy visible below md. -->
|
||||||
split — otherwise both instances render their own copy at the identical
|
<div
|
||||||
screen position simultaneously. -->
|
class="left-content pointer-events-none fixed right-sai z-popover flex justify-center bottom-[calc(4.5rem+var(--saib))] md:hidden">
|
||||||
<div
|
<CallControlBar {url} {h} />
|
||||||
class={cx(
|
</div>
|
||||||
"left-content pointer-events-none fixed right-sai z-popover justify-center bottom-[calc(4.5rem+var(--saib))] md:bottom-4",
|
{:else}
|
||||||
mobile ? "flex md:hidden" : "hidden md:flex",
|
<!-- desktopWrapperClass carries this instance's own visibility ("hidden ...
|
||||||
)}>
|
md:flex") and is `relative`, so the control bar below is `absolute` against
|
||||||
<CallControlBar {url} {h} />
|
the real (chat-sidebar-aware) video pane box rather than the viewport —
|
||||||
</div>
|
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}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,30 @@
|
||||||
@layer components {
|
@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 {
|
.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);
|
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 {
|
.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;
|
@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