Compare commits
4 Commits
master
...
top-bar-mobile
| Author | SHA1 | Date | |
|---|---|---|---|
| ce7223b0eb | |||
| ffd06ab561 | |||
| eb8dd330b6 | |||
| 6267e52bdf |
@@ -6,11 +6,13 @@ export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
room: LiveKitRoom
|
||||
muted: boolean
|
||||
cameraOn: boolean
|
||||
screenShareOn: boolean
|
||||
}
|
||||
|
||||
/** Mic mute state is separate so toggling it does not re-render video tiles. */
|
||||
export const voiceMicMuted = writable(true)
|
||||
|
||||
export type Pubkey = string
|
||||
|
||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type AudioCaptureOptions,
|
||||
} from "livekit-client"
|
||||
import {derived, get} from "svelte/store"
|
||||
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||
import {map, not, removeUndefined, uniqBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||
import {signer} from "@welshman/app"
|
||||
@@ -22,6 +22,7 @@ import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||
import {
|
||||
currentVoiceRoom,
|
||||
currentVoiceSession,
|
||||
voiceMicMuted,
|
||||
participantFromLiveKitIdentity,
|
||||
participantKey,
|
||||
participantPubkeyMap,
|
||||
@@ -173,6 +174,7 @@ const setUpMicrophone = async (
|
||||
|
||||
const onRoomDisconnected = (reason?: DisconnectReason) => {
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
voiceMicMuted.set(true)
|
||||
currentVoiceSession.set(undefined)
|
||||
resetVideoCallLayout()
|
||||
if (reason !== undefined && reason !== DisconnectReason.CLIENT_INITIATED) {
|
||||
@@ -295,11 +297,11 @@ export const joinVoiceRoom = async (
|
||||
|
||||
const muted = await setUpMicrophone(startMuted, preferredMicId, liveKitRoom.localParticipant)
|
||||
|
||||
voiceMicMuted.set(muted)
|
||||
currentVoiceSession.set({
|
||||
url,
|
||||
h,
|
||||
room: liveKitRoom,
|
||||
muted,
|
||||
cameraOn: false,
|
||||
screenShareOn: false,
|
||||
})
|
||||
@@ -339,6 +341,7 @@ export const leaveVoiceRoom = async () => {
|
||||
|
||||
voiceState.set(VoiceState.Disconnected)
|
||||
videoPrimaryTileKey.set(undefined)
|
||||
voiceMicMuted.set(true)
|
||||
currentVoiceSession.set(undefined)
|
||||
resetVideoCallLayout()
|
||||
session.room.disconnect()
|
||||
@@ -356,18 +359,17 @@ export const toggleMute = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const muted = !session.muted
|
||||
if (muted) {
|
||||
voiceMicMuted.update(not)
|
||||
if (get(voiceMicMuted)) {
|
||||
// Disable and re-enable microphone to trigger permission prompt
|
||||
session.room.localParticipant.setMicrophoneEnabled(false)
|
||||
currentVoiceSession.set({...session, muted})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setMicrophoneEnabled(true)
|
||||
currentVoiceSession.set({...session, muted})
|
||||
} catch (e) {
|
||||
voiceMicMuted.set(true)
|
||||
pushToast({theme: "error", message: "Could not access microphone"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
url: string
|
||||
size?: number
|
||||
fallbackIcon?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, h, size = 5, fallbackIcon = Hashtag}: Props = $props()
|
||||
const {url, h, size = 5, fallbackIcon = Hashtag, class: className}: Props = $props()
|
||||
|
||||
const room = deriveRoom(url, h)
|
||||
const isVoiceRoom = $derived($room.livekit)
|
||||
@@ -23,11 +24,11 @@
|
||||
<Icon size={size + 1} icon={Volume} />
|
||||
{#if $room.picture}
|
||||
<span class="text-base">/</span>
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||
<ImageIcon src={$room.picture} {size} alt="" class={className || "rounded-lg"} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $room.picture}
|
||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
||||
<ImageIcon src={$room.picture} {size} alt="" class={className || "rounded-lg"} />
|
||||
{:else}
|
||||
<Icon icon={fallbackIcon} {size} />
|
||||
<Icon icon={fallbackIcon} {size} class={className} />
|
||||
{/if}
|
||||
|
||||
@@ -5,17 +5,24 @@
|
||||
interface Props {
|
||||
h: string
|
||||
url: string
|
||||
imageClass?: string
|
||||
nameClass?: string
|
||||
size?: number
|
||||
subtitle?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {url, h, ...props}: Props = $props()
|
||||
const {url, h, imageClass, nameClass, size = 5, subtitle, ...props}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex grow items-center justify-between gap-4 {props.class}">
|
||||
<div class="flex items-center gap-2">
|
||||
<RoomImage {url} {h} />
|
||||
<div class="min-w-0 overflow-hidden text-ellipsis">
|
||||
<RoomName {url} {h} />
|
||||
<div class="flex min-w-0 items-center gap-2 {props.class}">
|
||||
<RoomImage {url} {h} {size} class={imageClass} />
|
||||
{#if subtitle}
|
||||
<div class="min-w-0">
|
||||
<RoomName {url} {h} class={nameClass} />
|
||||
<div class="truncate text-xs leading-4 text-primary">{subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<RoomName {url} {h} class={nameClass} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,29 +14,38 @@
|
||||
back?: () => unknown
|
||||
title?: Snippet
|
||||
action?: Snippet
|
||||
hideRelayUrl?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const {back = () => goto(makeSpacePath(url)), title, action, ...props}: Props = $props()
|
||||
const {
|
||||
back = () => goto(makeSpacePath(url)),
|
||||
title,
|
||||
action,
|
||||
hideRelayUrl = false,
|
||||
...props
|
||||
}: Props = $props()
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
</script>
|
||||
|
||||
<PageBar {...props}>
|
||||
<div class="flex">
|
||||
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
||||
<Icon icon={ArrowLeft} size={7} />
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1">
|
||||
<Button onclick={back} class="btn btn-ghost btn-square shrink-0 md:hidden">
|
||||
<Icon icon={ArrowLeft} size={6} />
|
||||
</Button>
|
||||
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex min-w-0 flex-1 items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
{@render title?.()}
|
||||
</div>
|
||||
<div class="text-xs text-primary md:hidden">
|
||||
{displayRelayUrl(url)}
|
||||
</div>
|
||||
{#if !hideRelayUrl}
|
||||
<div class="truncate text-xs leading-4 text-primary md:hidden">
|
||||
{displayRelayUrl(url)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2 items-start">
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
{@render action?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,6 +144,9 @@
|
||||
|
||||
const useSpotlightLayout = $derived(primaryTile !== undefined)
|
||||
const useMultiGrid = $derived(!useSpotlightLayout && videoTiles.length > 2)
|
||||
const multiGridClass = $derived(
|
||||
layout === VideoCallLayout.Split ? "grid-cols-1" : "grid-cols-1 sm:grid-cols-2",
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
const k = $videoPrimaryTileKey
|
||||
@@ -238,8 +241,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if useMultiGrid}
|
||||
<div
|
||||
class="grid min-h-0 flex-1 grid-cols-1 content-start gap-2 overflow-y-auto sm:grid-cols-2">
|
||||
<div class={cx("grid min-h-0 flex-1 content-start gap-2 overflow-y-auto", multiGridClass)}>
|
||||
{#each videoTiles as tile (tileKey(tile))}
|
||||
{@render videoTile(tile, "default")}
|
||||
{/each}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
VoiceState,
|
||||
currentVoiceSession,
|
||||
currentVoiceRoom,
|
||||
voiceMicMuted,
|
||||
voiceState,
|
||||
isLocalSpeaking,
|
||||
} from "@app/call/stores"
|
||||
@@ -183,18 +184,17 @@
|
||||
</Button>
|
||||
{:else if $voiceState === VoiceState.Connected && $currentVoiceSession}
|
||||
<Button
|
||||
data-tip={$currentVoiceSession.muted ? "Unmute" : "Mute"}
|
||||
data-tip={$voiceMicMuted ? "Unmute" : "Mute"}
|
||||
class={cx(
|
||||
mediaToggleClass,
|
||||
"overflow-visible",
|
||||
!$currentVoiceSession.muted && $isLocalSpeaking && "text-primary",
|
||||
$currentVoiceSession.muted &&
|
||||
"text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
||||
!$voiceMicMuted && $isLocalSpeaking && "text-primary",
|
||||
$voiceMicMuted && "text-error ring-1 ring-error/50 ring-offset-0 ring-offset-base-100",
|
||||
)}
|
||||
onclick={toggleMute}>
|
||||
<span class="relative inline-flex items-center justify-center overflow-visible">
|
||||
<Icon icon={Microphone} size={4} />
|
||||
{#if $currentVoiceSession.muted}
|
||||
{#if $voiceMicMuted}
|
||||
<span
|
||||
class="pointer-events-none absolute inset-0 flex items-center justify-center overflow-visible"
|
||||
aria-hidden="true">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {Badge} from "@capawesome/capacitor-badge"
|
||||
import {synced, throttled, withGetter} from "@welshman/store"
|
||||
import {pubkey, tracker, repository, relaysByUrl} from "@welshman/app"
|
||||
@@ -36,6 +36,9 @@ export const deriveChecked = (key: string) => derived(checked, prop<number>(key)
|
||||
|
||||
export const setChecked = (key: string) => checked.update(assoc(key, now()))
|
||||
|
||||
/** Room path while video call UI hides chat; checked + badge stay active until chat is shown. */
|
||||
export const deferredRoomPath = writable<string | undefined>(undefined)
|
||||
|
||||
export const syncChecked = () => {
|
||||
let prev = ""
|
||||
|
||||
@@ -57,8 +60,11 @@ export const syncChecked = () => {
|
||||
|
||||
// Set checked when we visit a given page - but delay it a tad
|
||||
setTimeout(() => {
|
||||
const defer = get(deferredRoomPath)
|
||||
|
||||
checked.update($checked => {
|
||||
for (const path of getPaths($page.url.pathname)) {
|
||||
if (defer && path === defer) continue
|
||||
$checked[path] = now()
|
||||
}
|
||||
|
||||
@@ -151,9 +157,17 @@ export const allNotifications = derived(
|
||||
},
|
||||
)
|
||||
|
||||
export const notifications = derived([page, allNotifications], ([$page, $allNotifications]) => {
|
||||
return new Set([...$allNotifications].filter(p => !$page.url.pathname.startsWith(p)))
|
||||
})
|
||||
export const notifications = derived(
|
||||
[page, allNotifications, deferredRoomPath],
|
||||
([$page, $allNotifications, $deferredRoomPath]) =>
|
||||
new Set(
|
||||
[...$allNotifications].filter(p => {
|
||||
if (!$page.url.pathname.startsWith(p)) return true
|
||||
if ($deferredRoomPath && p === $deferredRoomPath) return true
|
||||
return false
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// Badges
|
||||
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
const {children, ...props}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div data-component="PageBar" class="relative z-nav p-2 -mb-4 {props.class}">
|
||||
<div class="rounded-xl bg-base-100 p-4 shadow-md h-20 md:h-12 flex flex-col justify-center">
|
||||
<div
|
||||
data-component="PageBar"
|
||||
class="relative z-nav -mx-sai -mt-sai shrink-0 md:mx-0 md:-mb-4 md:mt-0 md:p-2 {props.class}">
|
||||
<div
|
||||
class="border-base-300 bg-base-100 flex min-h-[calc(4rem+var(--sait))] items-center border-b px-4 pb-2 pt-sai md:h-12 md:min-h-0 md:rounded-xl md:border-0 md:p-4 md:shadow-md">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {readable} from "svelte/store"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
@@ -9,7 +9,7 @@
|
||||
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||
import type {MakeNonOptional} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {makeEvent, makeRoomMeta, MESSAGE, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||
import {displayRelayUrl, makeEvent, makeRoomMeta, MESSAGE, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
||||
@@ -24,10 +24,9 @@
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
||||
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
||||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import RoomDetail from "@app/components/RoomDetail.svelte"
|
||||
import RoomItem from "@app/components/RoomItem.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||
@@ -49,7 +48,8 @@
|
||||
import {VideoCallLayout, videoCallLayout, videoTileCount} from "@app/call/video"
|
||||
import {makeFeed} from "@app/core/requests"
|
||||
import {popKey} from "@lib/implicit"
|
||||
import {checked} from "@app/util/notifications"
|
||||
import {checked, deferredRoomPath, setChecked} from "@app/util/notifications"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {pushModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
@@ -77,6 +77,21 @@
|
||||
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video,
|
||||
)
|
||||
|
||||
const roomPath = makeRoomPath(url, h)
|
||||
|
||||
const videoCallChatHidden = $derived(
|
||||
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video,
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
deferredRoomPath.set(videoCallChatHidden ? roomPath : undefined)
|
||||
if (voiceConnectedHere && !videoCallChatHidden) {
|
||||
setChecked(roomPath)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => deferredRoomPath.set(undefined))
|
||||
|
||||
let prevVideoTileCount = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
@@ -413,10 +428,17 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
<SpaceBar hideRelayUrl>
|
||||
{#snippet title()}
|
||||
<RoomImage {url} {h} />
|
||||
<RoomName {url} {h} />
|
||||
<RoomNameWithImage
|
||||
{url}
|
||||
{h}
|
||||
size={8}
|
||||
imageClass="rounded-full"
|
||||
nameClass="font-semibold"
|
||||
subtitle={displayRelayUrl(url)}
|
||||
class="md:hidden" />
|
||||
<RoomNameWithImage {url} {h} class="hidden md:flex" />
|
||||
{/snippet}
|
||||
{#snippet action()}
|
||||
<SpaceSearch {url} {h} />
|
||||
@@ -452,7 +474,7 @@
|
||||
bind:element
|
||||
onscroll={onScroll}
|
||||
class={cx(
|
||||
"flex-col-reverse pb-0! pt-4",
|
||||
"flex-col-reverse pb-0! pt-2 md:pt-4",
|
||||
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
||||
pageContentHiddenDesktopVideoOnly && "md:hidden",
|
||||
)}>
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4 pb-0!">
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-2 pb-0! md:pt-4">
|
||||
{#if loadingForward}
|
||||
<p class="py-20 flex justify-center">
|
||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||
|
||||
Reference in New Issue
Block a user