Factor video code and stores out of voice.ts
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import {Room as LiveKitRoom} from "livekit-client"
|
||||
import {derived, writable} from "svelte/store"
|
||||
import {type Room} from "@app/core/state"
|
||||
|
||||
export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
room: LiveKitRoom
|
||||
muted: boolean
|
||||
cameraOn: boolean
|
||||
screenShareOn: boolean
|
||||
}
|
||||
|
||||
export type Pubkey = string
|
||||
|
||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||
|
||||
export enum VoiceState {
|
||||
Joining = "joining",
|
||||
Connected = "connected",
|
||||
Disconnected = "disconnected",
|
||||
}
|
||||
|
||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||
|
||||
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||
|
||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||
|
||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||
|
||||
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||
|
||||
export const participantFromLiveKitIdentity = (identity: string): VoiceParticipant => {
|
||||
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||
return pk ? {pubkey: pk, identity} : {identity}
|
||||
}
|
||||
|
||||
export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
||||
|
||||
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||
|
||||
export const isParticipantSpeaking = derived(
|
||||
speakingParticipants,
|
||||
$participants => (p: VoiceParticipant) =>
|
||||
$participants.some(sp => participantKey(sp) === participantKey(p)),
|
||||
)
|
||||
|
||||
/** True when the local user is in LiveKit’s active-speakers list (currently talking). */
|
||||
export const isLocalSpeaking = derived(
|
||||
[currentVoiceSession, speakingParticipants],
|
||||
([$session, $speaking]) => {
|
||||
if (!$session?.room) return false
|
||||
const local = participantFromLiveKitIdentity($session.room.localParticipant.identity)
|
||||
return $speaking.some(sp => participantKey(sp) === participantKey(local))
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
import {Track} from "livekit-client"
|
||||
import {MediaQuery} from "svelte/reactivity"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {currentVoiceSession, VoiceState, type VoiceSession, voiceState} from "@app/call/stores"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export enum VideoCallLayout {
|
||||
Chat = "chat",
|
||||
Video = "video",
|
||||
Split = "split",
|
||||
}
|
||||
|
||||
export const isDesktopLayout = new MediaQuery("min-width: 768px", false)
|
||||
|
||||
export enum ViewportSize {
|
||||
Desktop = "desktop",
|
||||
Mobile = "mobile",
|
||||
}
|
||||
|
||||
export const videoCallViewportSync = {
|
||||
previousLayout: undefined as ViewportSize | undefined,
|
||||
}
|
||||
|
||||
export const videoCallLayout = writable<VideoCallLayout>(VideoCallLayout.Split)
|
||||
|
||||
export const resetVideoCallLayout = () => {
|
||||
videoCallViewportSync.previousLayout = undefined
|
||||
videoCallLayout.set(VideoCallLayout.Chat)
|
||||
}
|
||||
|
||||
export const videoPrimaryTileKey = writable<string | undefined>(undefined)
|
||||
|
||||
export const toggleVideoPrimaryTile = (key: string) => {
|
||||
videoPrimaryTileKey.update(k => (k === key ? undefined : key))
|
||||
}
|
||||
|
||||
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
|
||||
|
||||
const countLiveVisualFeeds = (session: VoiceSession): number => {
|
||||
const room = session.room
|
||||
let n = 0
|
||||
const lp = room.localParticipant
|
||||
if (session.cameraOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.Camera)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
if (session.screenShareOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.ScreenShare)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
for (const rp of room.remoteParticipants.values()) {
|
||||
for (const source of VISUAL_SOURCES) {
|
||||
const pub = rp.getTrackPublication(source)
|
||||
if (pub?.isSubscribed && pub.track) n += 1
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export const triggerVideoFeedCount = () => {
|
||||
currentVoiceSession.update(s => (s ? {...s} : s))
|
||||
}
|
||||
|
||||
export const videoTileCount = derived([currentVoiceSession, voiceState], ([$session, $state]) => {
|
||||
if ($state !== VoiceState.Connected || !$session) return 0
|
||||
return countLiveVisualFeeds($session)
|
||||
})
|
||||
|
||||
export const toggleCamera = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const cameraOn = !session.cameraOn
|
||||
if (!cameraOn) {
|
||||
session.room.localParticipant.setCameraEnabled(false)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setCameraEnabled(true)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
} catch (e) {
|
||||
pushToast({theme: "error", message: "Could not access camera"})
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleScreenShare = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const screenShareOn = !session.screenShareOn
|
||||
if (!screenShareOn) {
|
||||
session.room.localParticipant.setScreenShareEnabled(false)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setScreenShareEnabled(true)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
} catch (e) {
|
||||
pushToast({theme: "error", message: "Could not start screen sharing"})
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,27 @@ import {
|
||||
supportsAudioOutputSelection,
|
||||
type AudioCaptureOptions,
|
||||
} from "livekit-client"
|
||||
import {derived, get, writable} from "svelte/store"
|
||||
import {derived, get} from "svelte/store"
|
||||
import {map, removeUndefined, uniqBy} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||
import {signer} from "@welshman/app"
|
||||
import {getLivekitEndpoint} from "$lib/livekit"
|
||||
import {AbortError, whenAborted, whenTimeout} from "$lib/util"
|
||||
import {deriveLatestEventForUrl, deriveRoom, makeRoomId, type Room} from "@app/core/state"
|
||||
import {
|
||||
currentVoiceRoom,
|
||||
currentVoiceSession,
|
||||
participantFromLiveKitIdentity,
|
||||
participantKey,
|
||||
participantPubkeyMap,
|
||||
pubkeyFromLiveKitIdentity,
|
||||
speakingParticipants,
|
||||
VoiceState,
|
||||
type VoiceParticipant,
|
||||
voiceState,
|
||||
} from "@app/call/stores"
|
||||
import {resetVideoCallLayout, triggerVideoFeedCount, videoPrimaryTileKey} from "@app/call/video"
|
||||
import {deriveLatestEventForUrl, deriveRoom, makeRoomId} from "@app/core/state"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
export const LIVEKIT_PARTICIPANTS = 39004
|
||||
@@ -28,27 +41,6 @@ export {checkRelayHasLivekit} from "$lib/livekit"
|
||||
|
||||
export {supportsAudioOutputSelection}
|
||||
|
||||
export type VoiceSession = {
|
||||
url: string
|
||||
h: string
|
||||
room: LiveKitRoom
|
||||
muted: boolean
|
||||
cameraOn: boolean
|
||||
screenShareOn: boolean
|
||||
}
|
||||
|
||||
export type Pubkey = string
|
||||
|
||||
export type VoiceParticipant = {pubkey?: Pubkey; identity: string}
|
||||
|
||||
export enum VoiceState {
|
||||
Joining = "joining",
|
||||
Connected = "connected",
|
||||
Disconnected = "disconnected",
|
||||
}
|
||||
|
||||
export const currentVoiceSession = writable<VoiceSession | undefined>(undefined)
|
||||
|
||||
const LIVEKIT_DEFAULT_DEVICE_ID = "default"
|
||||
|
||||
export enum DeviceKind {
|
||||
@@ -83,36 +75,6 @@ export const switchVoiceActiveDevice = async (
|
||||
}
|
||||
}
|
||||
|
||||
export const voiceState = writable<VoiceState>(VoiceState.Disconnected)
|
||||
|
||||
export const currentVoiceRoom = writable<Room | undefined>(undefined)
|
||||
|
||||
/** Chat-only, full-width video, or split (desktop). On narrow viewports, `split` shows as chat until resize remaps it. */
|
||||
export enum VideoCallLayout {
|
||||
Chat = "chat",
|
||||
Video = "video",
|
||||
Split = "split",
|
||||
}
|
||||
|
||||
export const videoCallLayout = writable<VideoCallLayout>(VideoCallLayout.Split)
|
||||
|
||||
const resetVideoCallLayout = () => {
|
||||
videoCallLayout.set(VideoCallLayout.Chat)
|
||||
}
|
||||
|
||||
export const participantPubkeyMap = writable<Map<string, Pubkey>>(new Map())
|
||||
|
||||
/** Spotlight tile id — must match VideoCallContent `tileKey` (identity + source, not trackSid). */
|
||||
export const videoPrimaryTileKey = writable<string | undefined>(undefined)
|
||||
|
||||
export const toggleVideoPrimaryTile = (key: string) => {
|
||||
videoPrimaryTileKey.update(k => (k === key ? undefined : key))
|
||||
}
|
||||
|
||||
const triggerVideoTileCount = () => {
|
||||
currentVoiceSession.update(s => (s ? {...s} : s))
|
||||
}
|
||||
|
||||
const addParticipant = (identity: string) => {
|
||||
participantPubkeyMap.update(m => {
|
||||
const next = new Map(m)
|
||||
@@ -129,34 +91,6 @@ const deleteParticipant = (identity: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const pubkeyFromLiveKitIdentity = (identity: string): string | undefined =>
|
||||
/^[a-f0-9]{64}$/.test(identity.slice(0, 64)) ? identity.slice(0, 64) : undefined
|
||||
|
||||
export const participantFromLiveKitIdentity = (identity: string): VoiceParticipant => {
|
||||
const pk = pubkeyFromLiveKitIdentity(identity)
|
||||
return pk ? {pubkey: pk, identity} : {identity}
|
||||
}
|
||||
|
||||
export const participantKey = (p: VoiceParticipant) => p.pubkey ?? p.identity
|
||||
|
||||
export const speakingParticipants = writable<VoiceParticipant[]>([])
|
||||
|
||||
export const isParticipantSpeaking = derived(
|
||||
speakingParticipants,
|
||||
$participants => (p: VoiceParticipant) =>
|
||||
$participants.some(sp => participantKey(sp) === participantKey(p)),
|
||||
)
|
||||
|
||||
/** True when the local user is in LiveKit’s active-speakers list (currently talking). */
|
||||
export const isLocalSpeaking = derived(
|
||||
[currentVoiceSession, speakingParticipants],
|
||||
([$session, $speaking]) => {
|
||||
if (!$session?.room) return false
|
||||
const local = participantFromLiveKitIdentity($session.room.localParticipant.identity)
|
||||
return $speaking.some(sp => participantKey(sp) === participantKey(local))
|
||||
},
|
||||
)
|
||||
|
||||
const fetchLivekitToken = async (
|
||||
url: string,
|
||||
groupId: string,
|
||||
@@ -260,14 +194,14 @@ const onTrackSubscribed = (track: Track) => {
|
||||
document.body.appendChild(element)
|
||||
element.play().catch(() => {})
|
||||
} else if (track.kind === Track.Kind.Video) {
|
||||
triggerVideoTileCount()
|
||||
triggerVideoFeedCount()
|
||||
}
|
||||
}
|
||||
|
||||
const onTrackUnsubscribed = (track: Track) => {
|
||||
track.detach().forEach(el => el.remove())
|
||||
if (track.kind === Track.Kind.Video) {
|
||||
triggerVideoTileCount()
|
||||
triggerVideoFeedCount()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,69 +371,3 @@ export const toggleMute = async () => {
|
||||
pushToast({theme: "error", message: "Could not access microphone"})
|
||||
}
|
||||
}
|
||||
|
||||
const VISUAL_SOURCES = [Track.Source.Camera, Track.Source.ScreenShare] as const
|
||||
|
||||
const countLiveVisualFeeds = (session: VoiceSession): number => {
|
||||
const room = session.room
|
||||
let n = 0
|
||||
const lp = room.localParticipant
|
||||
if (session.cameraOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.Camera)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
if (session.screenShareOn) {
|
||||
const pub = lp.getTrackPublication(Track.Source.ScreenShare)
|
||||
if (pub?.track) n += 1
|
||||
}
|
||||
for (const rp of room.remoteParticipants.values()) {
|
||||
for (const source of VISUAL_SOURCES) {
|
||||
const pub = rp.getTrackPublication(source)
|
||||
if (pub?.isSubscribed && pub.track) n += 1
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export const videoTileCount = derived([currentVoiceSession, voiceState], ([$session, $state]) => {
|
||||
if ($state !== VoiceState.Connected || !$session) return 0
|
||||
return countLiveVisualFeeds($session)
|
||||
})
|
||||
|
||||
export const toggleCamera = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const cameraOn = !session.cameraOn
|
||||
if (!cameraOn) {
|
||||
session.room.localParticipant.setCameraEnabled(false)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setCameraEnabled(true)
|
||||
currentVoiceSession.set({...session, cameraOn})
|
||||
} catch (e) {
|
||||
pushToast({theme: "error", message: "Could not access camera"})
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleScreenShare = async () => {
|
||||
const session = get(currentVoiceSession)
|
||||
if (!session) return
|
||||
|
||||
const screenShareOn = !session.screenShareOn
|
||||
if (!screenShareOn) {
|
||||
session.room.localParticipant.setScreenShareEnabled(false)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await session.room.localParticipant.setScreenShareEnabled(true)
|
||||
currentVoiceSession.set({...session, screenShareOn})
|
||||
} catch (e) {
|
||||
pushToast({theme: "error", message: "Could not start screen sharing"})
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,17 @@
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import VideoCallVideo from "@app/components/VideoCallVideo.svelte"
|
||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||
import {get} from "svelte/store"
|
||||
import {
|
||||
currentVoiceSession,
|
||||
currentVoiceRoom,
|
||||
VideoCallLayout,
|
||||
videoPrimaryTileKey,
|
||||
isDesktopLayout,
|
||||
toggleVideoPrimaryTile,
|
||||
pubkeyFromLiveKitIdentity,
|
||||
} from "@app/voice"
|
||||
videoCallLayout,
|
||||
videoCallViewportSync,
|
||||
ViewportSize,
|
||||
videoPrimaryTileKey,
|
||||
} from "@app/call/video"
|
||||
import {currentVoiceSession, currentVoiceRoom, pubkeyFromLiveKitIdentity} from "@app/call/stores"
|
||||
|
||||
type Props = {
|
||||
layout: VideoCallLayout
|
||||
@@ -37,6 +40,23 @@
|
||||
|
||||
const {layout, mobile = false, url, h, class: className = ""}: Props = $props()
|
||||
|
||||
$effect(() => {
|
||||
const currentLayout = isDesktopLayout.current ? ViewportSize.Desktop : ViewportSize.Mobile
|
||||
const {previousLayout} = videoCallViewportSync
|
||||
if (previousLayout === undefined) {
|
||||
videoCallViewportSync.previousLayout = currentLayout
|
||||
return
|
||||
}
|
||||
if (previousLayout === currentLayout) return
|
||||
const p = get(videoCallLayout)
|
||||
if (previousLayout === ViewportSize.Desktop && currentLayout === ViewportSize.Mobile) {
|
||||
if (p === VideoCallLayout.Split) videoCallLayout.set(VideoCallLayout.Video)
|
||||
} else if (previousLayout === ViewportSize.Mobile && currentLayout === ViewportSize.Desktop) {
|
||||
if (p === VideoCallLayout.Chat) videoCallLayout.set(VideoCallLayout.Split)
|
||||
}
|
||||
videoCallViewportSync.previousLayout = currentLayout
|
||||
})
|
||||
|
||||
const isViewingCurrentCallRoom = $derived(
|
||||
$currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h,
|
||||
)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<script module lang="ts">
|
||||
import {MediaQuery} from "svelte/reactivity"
|
||||
|
||||
export const isDesktopLayout = new MediaQuery("min-width: 768px", false)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {get} from "svelte/store"
|
||||
import {VideoCallLayout, videoCallLayout} from "@app/voice"
|
||||
|
||||
let prevLayout = $state<boolean | undefined>(undefined)
|
||||
|
||||
$effect(() => {
|
||||
const currentLayout = isDesktopLayout.current
|
||||
if (prevLayout === undefined) {
|
||||
prevLayout = currentLayout
|
||||
return
|
||||
}
|
||||
if (prevLayout === currentLayout) return
|
||||
const p = get(videoCallLayout)
|
||||
if (prevLayout && !currentLayout) {
|
||||
if (p === VideoCallLayout.Split) videoCallLayout.set(VideoCallLayout.Video)
|
||||
} else if (!prevLayout && currentLayout) {
|
||||
if (p === VideoCallLayout.Chat) videoCallLayout.set(VideoCallLayout.Split)
|
||||
}
|
||||
prevLayout = currentLayout
|
||||
})
|
||||
</script>
|
||||
@@ -7,13 +7,8 @@
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import {
|
||||
currentVoiceSession,
|
||||
DeviceKind,
|
||||
supportsAudioOutputSelection,
|
||||
switchVoiceActiveDevice,
|
||||
type VoiceSession,
|
||||
} from "@app/voice"
|
||||
import {currentVoiceSession, type VoiceSession} from "@app/call/stores"
|
||||
import {DeviceKind, supportsAudioOutputSelection, switchVoiceActiveDevice} from "@app/call/voice"
|
||||
import {popModal} from "@app/util/modal"
|
||||
|
||||
const selectValueForActiveDevice = (session: VoiceSession, kind: DeviceKind): string => {
|
||||
|
||||
@@ -12,14 +12,13 @@
|
||||
import {makeRoomId} from "@app/core/state"
|
||||
import {
|
||||
VoiceState,
|
||||
deriveVoiceParticipants,
|
||||
cancelJoinVoiceRoom,
|
||||
currentVoiceRoom,
|
||||
voiceState,
|
||||
isParticipantSpeaking,
|
||||
participantKey,
|
||||
voiceState,
|
||||
type VoiceParticipant,
|
||||
} from "@app/voice"
|
||||
} from "@app/call/stores"
|
||||
import {cancelJoinVoiceRoom, deriveVoiceParticipants} from "@app/call/voice"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import {AbortError, TimeoutError} from "$lib/util"
|
||||
import {displayRoom} from "@app/core/state"
|
||||
import {joinVoiceRoom} from "@app/voice"
|
||||
import {joinVoiceRoom} from "@app/call/voice"
|
||||
import {popModal} from "@app/util/modal"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import VoiceCallAudioSettingsDialog from "@app/components/VoiceCallAudioSettingsDialog.svelte"
|
||||
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||
import {isDesktopLayout} from "@app/components/VideoCallLayoutViewportSync.svelte"
|
||||
import {
|
||||
decodeRelay,
|
||||
deriveRoom,
|
||||
@@ -32,19 +31,20 @@
|
||||
import {notifications} from "@app/util/notifications"
|
||||
import {makeRoomPath} from "@app/util/routes"
|
||||
import {
|
||||
VoiceState,
|
||||
VideoCallLayout,
|
||||
isDesktopLayout,
|
||||
toggleCamera,
|
||||
toggleScreenShare,
|
||||
videoCallLayout,
|
||||
} from "@app/call/video"
|
||||
import {
|
||||
VoiceState,
|
||||
currentVoiceSession,
|
||||
currentVoiceRoom,
|
||||
voiceState,
|
||||
videoCallLayout,
|
||||
isLocalSpeaking,
|
||||
leaveVoiceRoom,
|
||||
toggleMute,
|
||||
toggleCamera,
|
||||
toggleScreenShare,
|
||||
cancelJoinVoiceRoom,
|
||||
} from "@app/voice"
|
||||
} from "@app/call/stores"
|
||||
import {cancelJoinVoiceRoom, leaveVoiceRoom, toggleMute} from "@app/call/voice"
|
||||
|
||||
const {relay, h} = $derived($page.params)
|
||||
const url = $derived(relay ? decodeRelay(relay) : undefined)
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
loadFeedsForPubkey,
|
||||
} from "@app/core/state"
|
||||
import {hasBlossomSupport} from "@app/core/commands"
|
||||
import {LIVEKIT_PARTICIPANTS} from "@app/voice"
|
||||
import {LIVEKIT_PARTICIPANTS} from "@app/call/voice"
|
||||
|
||||
// Utils
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
import {syncKeyboard} from "@app/util/keyboard"
|
||||
import {getPageTitle} from "@app/util/title"
|
||||
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
|
||||
import VideoCallLayoutViewportSync from "@app/components/VideoCallLayoutViewportSync.svelte"
|
||||
|
||||
const {children} = $props()
|
||||
|
||||
@@ -230,6 +229,5 @@
|
||||
<ModalContainer />
|
||||
<div class="tippy-target"></div>
|
||||
<NewNotificationSound />
|
||||
<VideoCallLayoutViewportSync />
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
@@ -45,14 +45,8 @@
|
||||
} from "@app/core/state"
|
||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||
import VideoCallContent from "@app/components/VideoCallContent.svelte"
|
||||
import {
|
||||
VoiceState,
|
||||
VideoCallLayout,
|
||||
currentVoiceRoom,
|
||||
videoTileCount,
|
||||
videoCallLayout,
|
||||
voiceState,
|
||||
} from "@app/voice"
|
||||
import {VoiceState, currentVoiceRoom, voiceState} from "@app/call/stores"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user