mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-15 05:03:34 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 365f2efb2e |
@@ -8,13 +8,6 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [1.25.2] - 2026-08-06
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- 🐛(frontend) serve MediaPipe assets under a versioned path
|
|
||||||
- 🐛(frontend) harmonize cache configuration for MediaPipe assets
|
|
||||||
|
|
||||||
## [1.25.1] - 2026-08-06
|
## [1.25.1] - 2026-08-06
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -65,11 +65,6 @@ server {
|
|||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /assets/mediapipe/wasm/ {
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serve static files with caching
|
# Serve static files with caching
|
||||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.25.2"
|
version = "1.25.1"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livekit-agents==1.6.7",
|
"livekit-agents==1.6.7",
|
||||||
|
|||||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agents"
|
name = "agents"
|
||||||
version = "1.25.2"
|
version = "1.25.1"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "livekit-agents" },
|
{ name = "livekit-agents" },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.25.2"
|
version = "1.25.1"
|
||||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1187,7 +1187,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "meet"
|
name = "meet"
|
||||||
version = "1.25.2"
|
version = "1.25.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ server {
|
|||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
location ^~ /assets/mediapipe/wasm/ {
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serve static files with caching
|
# Serve static files with caching
|
||||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
|||||||
Generated
+12
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||||
"@fontsource-variable/lexend": "5.2.11",
|
"@fontsource-variable/lexend": "5.2.11",
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"@tanstack/react-query": "5.101.1",
|
"@tanstack/react-query": "5.101.1",
|
||||||
"@timephy/rnnoise-wasm": "1.0.0",
|
"@timephy/rnnoise-wasm": "1.0.0",
|
||||||
"crisp-sdk-web": "1.1.2",
|
"crisp-sdk-web": "1.1.2",
|
||||||
|
"derive-valtio": "0.2.0",
|
||||||
"hoofd": "1.7.3",
|
"hoofd": "1.7.3",
|
||||||
"humanize-duration": "3.33.2",
|
"humanize-duration": "3.33.2",
|
||||||
"i18next": "26.3.1",
|
"i18next": "26.3.1",
|
||||||
@@ -4709,6 +4710,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/derive-valtio": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-6slhaFHtfaL3t5dLYaQt6s4G2xZymhu0Ktdl7OMeVk8+46RgR8ft6FL0Tr4F31W+yPH03nJe1SSP4JFy2hSMRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"valtio": ">=2.0.0-rc.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "meet",
|
"name": "meet",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "panda codegen && vite",
|
"dev": "panda codegen && vite",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"@tanstack/react-query": "5.101.1",
|
"@tanstack/react-query": "5.101.1",
|
||||||
"@timephy/rnnoise-wasm": "1.0.0",
|
"@timephy/rnnoise-wasm": "1.0.0",
|
||||||
"crisp-sdk-web": "1.1.2",
|
"crisp-sdk-web": "1.1.2",
|
||||||
|
"derive-valtio": "0.2.0",
|
||||||
"hoofd": "1.7.3",
|
"hoofd": "1.7.3",
|
||||||
"humanize-duration": "3.33.2",
|
"humanize-duration": "3.33.2",
|
||||||
"i18next": "26.3.1",
|
"i18next": "26.3.1",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Button } from '@/primitives'
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const SoundTester = () => {
|
export const SoundTester = () => {
|
||||||
const { t } = useTranslation('settings')
|
const { t } = useTranslation('settings')
|
||||||
@@ -16,10 +15,7 @@ export const SoundTester = () => {
|
|||||||
try {
|
try {
|
||||||
await audioRef?.current?.setSinkId(deviceId)
|
await audioRef?.current?.setSinkId(deviceId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError(
|
console.error(`Error setting sinkId: ${error}`)
|
||||||
'device_switch_failure',
|
|
||||||
new Error(`Error setting sinkId: ${error}`)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateActiveId(activeDeviceId)
|
updateActiveId(activeDeviceId)
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useLocation } from 'wouter'
|
import { useLocation } from 'wouter'
|
||||||
|
import { type PostHog } from 'posthog-js'
|
||||||
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
||||||
import { useUser } from '@/features/auth/api/useUser'
|
import { useUser } from '@/features/auth/api/useUser'
|
||||||
import { getPosthog } from '../utils'
|
|
||||||
|
let posthog: PostHog | null = null
|
||||||
|
|
||||||
|
const getPosthog = async () => {
|
||||||
|
if (!posthog) posthog = (await import('posthog-js')).default
|
||||||
|
return posthog
|
||||||
|
}
|
||||||
|
|
||||||
export const startAnalyticsSession = (data: ApiUser) => {
|
export const startAnalyticsSession = (data: ApiUser) => {
|
||||||
getPosthog().then((ph) => {
|
getPosthog().then((ph) => {
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
import { getPosthog } from './utils'
|
|
||||||
|
|
||||||
export const captureEvent = (
|
|
||||||
event: string,
|
|
||||||
props?: Record<string, unknown>
|
|
||||||
) => {
|
|
||||||
void getPosthog()
|
|
||||||
.then((ph) => {
|
|
||||||
ph.capture(event, props)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
/* telemetry must never break the app */
|
|
||||||
})
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.warn(`[telemetry] ${event}`, props)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LogCode =
|
|
||||||
// media
|
|
||||||
| 'join_preview_failure'
|
|
||||||
| 'livekit_room_error'
|
|
||||||
| 'device_switch_failure'
|
|
||||||
| 'permission_poll_failure'
|
|
||||||
// non-media families
|
|
||||||
| 'participant_mute_api_failure'
|
|
||||||
| 'permissions_api_failure'
|
|
||||||
| 'effects_processor_failure'
|
|
||||||
| 'clipboard_failure'
|
|
||||||
| 'fullscreen_failure'
|
|
||||||
| 'publish_sources_failure'
|
|
||||||
| 'disconnect_failure'
|
|
||||||
| 'generic_failure'
|
|
||||||
|
|
||||||
export const reportError = (
|
|
||||||
logCode: LogCode,
|
|
||||||
error: unknown,
|
|
||||||
extraInfo: Record<string, unknown> = {}
|
|
||||||
): void => {
|
|
||||||
const e = error instanceof Error ? error : new Error(String(error))
|
|
||||||
void getPosthog()
|
|
||||||
.then((ph) => {
|
|
||||||
ph.captureException(e, {
|
|
||||||
log_code: logCode,
|
|
||||||
error_name: e.name,
|
|
||||||
error_message: e.message,
|
|
||||||
...extraInfo,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
console.warn(`[${logCode}]`, e, extraInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeviceSnapshot {
|
|
||||||
cam_count: number
|
|
||||||
mic_count: number
|
|
||||||
out_count: number
|
|
||||||
labels_visible: boolean
|
|
||||||
saved_cam_present: boolean | null
|
|
||||||
saved_mic_present: boolean | null
|
|
||||||
saved_video_device_id_set: boolean
|
|
||||||
saved_audio_device_id_set: boolean
|
|
||||||
audio_enabled: boolean | null
|
|
||||||
video_enabled: boolean | null
|
|
||||||
cam_permission: PermissionState | 'unknown'
|
|
||||||
mic_permission: PermissionState | 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Reads the persisted LiveKit user choices without importing the store. */
|
|
||||||
const readPersistedChoices = (): {
|
|
||||||
videoDeviceId?: string
|
|
||||||
audioDeviceId?: string
|
|
||||||
videoEnabled?: boolean
|
|
||||||
audioEnabled?: boolean
|
|
||||||
} => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem('lk-user-choices') ?? '{}')
|
|
||||||
} catch {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryPermission = async (
|
|
||||||
name: 'camera' | 'microphone'
|
|
||||||
): Promise<PermissionState | 'unknown'> => {
|
|
||||||
try {
|
|
||||||
const status = await navigator.permissions.query({
|
|
||||||
name: name as PermissionName,
|
|
||||||
})
|
|
||||||
return status.state
|
|
||||||
} catch {
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deviceSnapshot = async (): Promise<DeviceSnapshot> => {
|
|
||||||
const choices = readPersistedChoices()
|
|
||||||
let devices: MediaDeviceInfo[] = []
|
|
||||||
try {
|
|
||||||
devices = await navigator.mediaDevices.enumerateDevices()
|
|
||||||
} catch {
|
|
||||||
/* snapshot stays partial */
|
|
||||||
}
|
|
||||||
const ofKind = (k: MediaDeviceKind) => devices.filter((d) => d.kind === k)
|
|
||||||
const present = (k: MediaDeviceKind, id?: string) =>
|
|
||||||
id ? ofKind(k).some((d) => d.deviceId === id) : null
|
|
||||||
|
|
||||||
const [cam_permission, mic_permission] = await Promise.all([
|
|
||||||
queryPermission('camera'),
|
|
||||||
queryPermission('microphone'),
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
|
||||||
cam_count: ofKind('videoinput').length,
|
|
||||||
mic_count: ofKind('audioinput').length,
|
|
||||||
out_count: ofKind('audiooutput').length,
|
|
||||||
labels_visible: devices.some((d) => !!d.label),
|
|
||||||
saved_cam_present: present('videoinput', choices.videoDeviceId),
|
|
||||||
saved_mic_present: present('audioinput', choices.audioDeviceId),
|
|
||||||
saved_video_device_id_set: !!choices.videoDeviceId,
|
|
||||||
saved_audio_device_id_set: !!choices.audioDeviceId,
|
|
||||||
audio_enabled: choices.audioEnabled ?? null,
|
|
||||||
video_enabled: choices.videoEnabled ?? null,
|
|
||||||
cam_permission,
|
|
||||||
mic_permission,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const captureMediaEvent = async (
|
|
||||||
event:
|
|
||||||
| 'media-device-error'
|
|
||||||
| 'media-acquisition'
|
|
||||||
| 'media-device-topology'
|
|
||||||
| 'media-device-success',
|
|
||||||
props: Record<string, unknown>
|
|
||||||
) => {
|
|
||||||
captureEvent(event, { ...props, ...(await deviceSnapshot()) })
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import type { PostHog } from 'posthog-js'
|
|
||||||
|
|
||||||
let posthog: PostHog | null = null
|
|
||||||
|
|
||||||
export const getPosthog = async () => {
|
|
||||||
if (!posthog) posthog = (await import('posthog-js')).default
|
|
||||||
return posthog
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ import { css } from '@/styled-system/css'
|
|||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { LoginButton } from '@/components/LoginButton'
|
import { LoginButton } from '@/components/LoginButton'
|
||||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
@@ -161,9 +160,7 @@ const Home = () => {
|
|||||||
window.location.replace(data.external_home_url)
|
window.location.replace(data.external_home_url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setRedirectFailed(true)
|
setRedirectFailed(true)
|
||||||
reportError('generic_failure', error, {
|
console.error('Site is not reachable:', error)
|
||||||
context: 'Site is not reachable:',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NotificationDuration } from './NotificationDuration'
|
|||||||
import type { Participant } from 'livekit-client'
|
import type { Participant } from 'livekit-client'
|
||||||
import type { NotificationPayload } from './NotificationPayload'
|
import type { NotificationPayload } from './NotificationPayload'
|
||||||
import type { RecordingMode } from '@/features/recording'
|
import type { RecordingMode } from '@/features/recording'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const notifyAutoMutedOnJoin = () => {
|
export const notifyAutoMutedOnJoin = () => {
|
||||||
toastQueue.add(
|
toastQueue.add(
|
||||||
@@ -56,9 +55,7 @@ export const decodeNotificationDataReceived = (
|
|||||||
return parsed as NotificationPayload
|
return parsed as NotificationPayload
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle errors appropriately for your application
|
// Handle errors appropriately for your application
|
||||||
reportError('generic_failure', error, {
|
console.error('Failed to decode notification payload:', error)
|
||||||
context: 'Failed to decode notification payload:',
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Participant } from 'livekit-client'
|
import type { Participant } from 'livekit-client'
|
||||||
import { useLowerHandParticipant } from './lowerHandParticipant'
|
import { useLowerHandParticipant } from './lowerHandParticipant'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const useLowerHandParticipants = () => {
|
export const useLowerHandParticipants = () => {
|
||||||
const { lowerHandParticipant } = useLowerHandParticipant()
|
const { lowerHandParticipant } = useLowerHandParticipant()
|
||||||
@@ -12,9 +11,7 @@ export const useLowerHandParticipants = () => {
|
|||||||
)
|
)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('generic_failure', error, {
|
console.error('An error occurred while lowering hands :', error)
|
||||||
context: 'An error occurred while lowering hands :',
|
|
||||||
})
|
|
||||||
throw new Error('An error occurred while lowering hands.', {
|
throw new Error('An error occurred while lowering hands.', {
|
||||||
cause: error,
|
cause: error,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { fetchApi } from '@/api/fetchApi'
|
import { fetchApi } from '@/api/fetchApi'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { AssignableParticipantRole } from '@/features/rooms/api/ApiRoom'
|
import { AssignableParticipantRole } from '@/features/rooms/api/ApiRoom'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const useParticipantRole = () => {
|
export const useParticipantRole = () => {
|
||||||
const data = useRoomData()
|
const data = useRoomData()
|
||||||
@@ -23,11 +22,8 @@ export const useParticipantRole = () => {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError(
|
console.error(
|
||||||
'generic_failure',
|
`Failed to update participant's role ${identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
new Error(
|
|
||||||
`Failed to update participant's role ${identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from '../../participants/api/listWaitingParticipants'
|
} from '../../participants/api/listWaitingParticipants'
|
||||||
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
|
import { decodeNotificationDataReceived } from '@/features/notifications/utils'
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
import { NotificationType } from '@/features/notifications/NotificationType'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const POLL_INTERVAL_MS = 1000
|
export const POLL_INTERVAL_MS = 1000
|
||||||
|
|
||||||
@@ -88,7 +87,7 @@ export const useWaitingParticipants = () => {
|
|||||||
|
|
||||||
await refetchWaiting()
|
await refetchWaiting()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError('generic_failure', e)
|
console.error(e)
|
||||||
setListEnabled(true)
|
setListEnabled(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { useEffect, useMemo } from 'react'
|
|||||||
import { CrossDocumentOverlaysContext } from '@/primitives/CrossDocumentOverlaysContext'
|
import { CrossDocumentOverlaysContext } from '@/primitives/CrossDocumentOverlaysContext'
|
||||||
|
|
||||||
const InternalPortal = ({ children }: { children: React.ReactNode }) => {
|
const InternalPortal = ({ children }: { children: React.ReactNode }) => {
|
||||||
const pipStoreSnap = useSnapshot(documentPictureInPictureStore, {
|
const pipStoreSnap = useSnapshot(documentPictureInPictureStore)
|
||||||
sync: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const container = useMemo(() => {
|
const container = useMemo(() => {
|
||||||
return pipStoreSnap?.window?.document.getElementById('root')
|
return pipStoreSnap?.window?.document.getElementById('root')
|
||||||
@@ -21,7 +19,7 @@ const InternalPortal = ({ children }: { children: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!container || !container.isConnected) return null
|
if (!container) return null
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { ref, useSnapshot } from 'valtio'
|
import { ref, useSnapshot } from 'valtio'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { flushSync } from 'react-dom'
|
|
||||||
import { documentPictureInPictureStore } from '@/stores/documentPictureInPicture'
|
import { documentPictureInPictureStore } from '@/stores/documentPictureInPicture'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const IS_PIP_SUPPORTED =
|
export const IS_PIP_SUPPORTED =
|
||||||
typeof globalThis !== 'undefined' && 'documentPictureInPicture' in globalThis
|
typeof globalThis !== 'undefined' && 'documentPictureInPicture' in globalThis
|
||||||
@@ -75,9 +73,7 @@ export const usePictureInPicture = () => {
|
|||||||
|
|
||||||
const cleanUp = () => {
|
const cleanUp = () => {
|
||||||
if (documentPictureInPictureStore.window === pipWindow) {
|
if (documentPictureInPictureStore.window === pipWindow) {
|
||||||
flushSync(() => {
|
documentPictureInPictureStore.window = null
|
||||||
documentPictureInPictureStore.window = null
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pipWindow.addEventListener('pagehide', () => cleanUp(), { once: true })
|
pipWindow.addEventListener('pagehide', () => cleanUp(), { once: true })
|
||||||
@@ -87,9 +83,7 @@ export const usePictureInPicture = () => {
|
|||||||
documentPictureInPictureStore.window = ref(pipWindow)
|
documentPictureInPictureStore.window = ref(pipWindow)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Avoid unhandled rejections if the user blocks or closes the request.
|
// Avoid unhandled rejections if the user blocks or closes the request.
|
||||||
reportError('generic_failure', error, {
|
console.error('Failed to open Picture-in-Picture window', error)
|
||||||
context: 'Failed to open Picture-in-Picture window',
|
|
||||||
})
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
notifyRecordingSaveInProgress,
|
notifyRecordingSaveInProgress,
|
||||||
useNotifyParticipants,
|
useNotifyParticipants,
|
||||||
} from '@/features/notifications'
|
} from '@/features/notifications'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { NoAccessView } from './NoAccessView'
|
import { NoAccessView } from './NoAccessView'
|
||||||
import { ControlsButton } from './ControlsButton'
|
import { ControlsButton } from './ControlsButton'
|
||||||
@@ -28,7 +29,6 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
|||||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
||||||
import { FeatureFlags } from '@/features/analytics/enums'
|
import { FeatureFlags } from '@/features/analytics/enums'
|
||||||
import { LimitDescription } from './LimitDescription'
|
import { LimitDescription } from './LimitDescription'
|
||||||
import { captureEvent, reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const ScreenRecordingSidePanel = () => {
|
export const ScreenRecordingSidePanel = () => {
|
||||||
const { data } = useConfig()
|
const { data } = useConfig()
|
||||||
@@ -63,7 +63,7 @@ export const ScreenRecordingSidePanel = () => {
|
|||||||
await notifyParticipants({
|
await notifyParticipants({
|
||||||
type: NotificationType.ScreenRecordingRequested,
|
type: NotificationType.ScreenRecordingRequested,
|
||||||
})
|
})
|
||||||
captureEvent('screen-recording-requested', {})
|
posthog.capture('screen-recording-requested', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScreenRecording = async () => {
|
const handleScreenRecording = async () => {
|
||||||
@@ -100,15 +100,13 @@ export const ScreenRecordingSidePanel = () => {
|
|||||||
await notifyParticipants({
|
await notifyParticipants({
|
||||||
type: NotificationType.ScreenRecordingStarted,
|
type: NotificationType.ScreenRecordingStarted,
|
||||||
})
|
})
|
||||||
captureEvent('screen-recording-started', {
|
posthog.capture('screen-recording-started', {
|
||||||
includeTranscript: includeTranscript,
|
includeTranscript: includeTranscript,
|
||||||
language: selectedLanguageKey,
|
language: selectedLanguageKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('generic_failure', error, {
|
console.error('Failed to handle recording:', error)
|
||||||
context: 'Failed to handle recording:',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
useNotifyParticipants,
|
useNotifyParticipants,
|
||||||
notifyRecordingSaveInProgress,
|
notifyRecordingSaveInProgress,
|
||||||
} from '@/features/notifications'
|
} from '@/features/notifications'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { VStack } from '@/styled-system/jsx'
|
import { VStack } from '@/styled-system/jsx'
|
||||||
import { Checkbox } from '@/primitives/Checkbox.tsx'
|
import { Checkbox } from '@/primitives/Checkbox.tsx'
|
||||||
@@ -34,7 +35,6 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
|||||||
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
|
||||||
import { LimitDescription } from './LimitDescription'
|
import { LimitDescription } from './LimitDescription'
|
||||||
import { openSettingsDialog } from '@/stores/settings'
|
import { openSettingsDialog } from '@/stores/settings'
|
||||||
import { captureEvent, reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const TranscriptSidePanel = () => {
|
export const TranscriptSidePanel = () => {
|
||||||
const { data } = useConfig()
|
const { data } = useConfig()
|
||||||
@@ -76,7 +76,7 @@ export const TranscriptSidePanel = () => {
|
|||||||
await notifyParticipants({
|
await notifyParticipants({
|
||||||
type: NotificationType.TranscriptionRequested,
|
type: NotificationType.TranscriptionRequested,
|
||||||
})
|
})
|
||||||
captureEvent('transcript-requested', {})
|
posthog.capture('transcript-requested', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTranscript = async () => {
|
const handleTranscript = async () => {
|
||||||
@@ -121,15 +121,13 @@ export const TranscriptSidePanel = () => {
|
|||||||
await notifyParticipants({
|
await notifyParticipants({
|
||||||
type: NotificationType.TranscriptionStarted,
|
type: NotificationType.TranscriptionStarted,
|
||||||
})
|
})
|
||||||
captureEvent('transcript-started', {
|
posthog.capture('transcript-started', {
|
||||||
includeScreenRecording: includeScreenRecording,
|
includeScreenRecording: includeScreenRecording,
|
||||||
language: selectedLanguageKey,
|
language: selectedLanguageKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('generic_failure', error, {
|
console.error('Failed to handle transcript:', error)
|
||||||
context: 'Failed to handle transcript:',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useRoomInfo } from '@livekit/components-react'
|
import { useRoomInfo } from '@livekit/components-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const useRoomMetadata = () => {
|
export const useRoomMetadata = () => {
|
||||||
const { metadata } = useRoomInfo()
|
const { metadata } = useRoomInfo()
|
||||||
@@ -9,9 +8,7 @@ export const useRoomMetadata = () => {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(metadata)
|
return JSON.parse(metadata)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('generic_failure', error, {
|
console.error('Failed to parse room metadata:', error)
|
||||||
context: 'Failed to parse room metadata:',
|
|
||||||
})
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { fetchApi } from '@/api/fetchApi'
|
|||||||
import { useIsAdminOrOwner } from '../livekit/hooks/useIsAdminOrOwner'
|
import { useIsAdminOrOwner } from '../livekit/hooks/useIsAdminOrOwner'
|
||||||
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const useMuteParticipant = () => {
|
export const useMuteParticipant = () => {
|
||||||
const apiRoomData = useRoomData()
|
const apiRoomData = useRoomData()
|
||||||
@@ -32,10 +31,7 @@ export const useMuteParticipant = () => {
|
|||||||
|
|
||||||
// Guard against undefined token for non-admin users
|
// Guard against undefined token for non-admin users
|
||||||
if (!isAdminOrOwner && !apiRoomData.livekit.token) {
|
if (!isAdminOrOwner && !apiRoomData.livekit.token) {
|
||||||
reportError(
|
console.error('Cannot mute participant: missing auth token')
|
||||||
'participant_mute_api_failure',
|
|
||||||
new Error('Cannot mute participant: missing auth token')
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +53,8 @@ export const useMuteParticipant = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError(
|
console.error(
|
||||||
'participant_mute_api_failure',
|
`Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
new Error(
|
|
||||||
`Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,11 +65,8 @@ export const useMuteParticipant = () => {
|
|||||||
destinationIdentities: [participant.identity],
|
destinationIdentities: [participant.identity],
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError(
|
console.error(
|
||||||
'participant_mute_api_failure',
|
`Failed to notify muted participant ${participant.identity}: ${e}`
|
||||||
new Error(
|
|
||||||
`Failed to notify muted participant ${participant.identity}: ${e}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Participant } from 'livekit-client'
|
import type { Participant } from 'livekit-client'
|
||||||
import { useMuteParticipant } from './muteParticipant'
|
import { useMuteParticipant } from './muteParticipant'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const useMuteParticipants = () => {
|
export const useMuteParticipants = () => {
|
||||||
const { muteParticipant } = useMuteParticipant()
|
const { muteParticipant } = useMuteParticipant()
|
||||||
@@ -12,9 +11,7 @@ export const useMuteParticipants = () => {
|
|||||||
)
|
)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('participant_mute_api_failure', error, {
|
console.error('An error occurred while muting participants :', error)
|
||||||
context: 'An error occurred while muting participants :',
|
|
||||||
})
|
|
||||||
throw new Error('An error occurred while muting participants.', {
|
throw new Error('An error occurred while muting participants.', {
|
||||||
cause: error,
|
cause: error,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Participant, Track } from 'livekit-client'
|
import type { Participant, Track } from 'livekit-client'
|
||||||
import { fetchApi } from '@/api/fetchApi'
|
import { fetchApi } from '@/api/fetchApi'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
type Source = Track.Source
|
type Source = Track.Source
|
||||||
|
|
||||||
export const useParticipantPermissions = () => {
|
export const useParticipantPermissions = () => {
|
||||||
@@ -33,11 +32,8 @@ export const useParticipantPermissions = () => {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError(
|
console.error(
|
||||||
'permissions_api_failure',
|
`Failed to update participant's permissions ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
new Error(
|
|
||||||
`Failed to update participant's permissions ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Participant, Track } from 'livekit-client'
|
import type { Participant, Track } from 'livekit-client'
|
||||||
import { useParticipantPermissions } from './updateParticipantPermissions'
|
import { useParticipantPermissions } from './updateParticipantPermissions'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
type Source = Track.Source
|
type Source = Track.Source
|
||||||
|
|
||||||
export const useUpdateParticipantsPermissions = () => {
|
export const useUpdateParticipantsPermissions = () => {
|
||||||
@@ -16,9 +15,7 @@ export const useUpdateParticipantsPermissions = () => {
|
|||||||
)
|
)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('permissions_api_failure', error, {
|
console.error('An error occurred while updating permissions :', error)
|
||||||
context: 'An error occurred while updating permissions :',
|
|
||||||
})
|
|
||||||
throw new Error('An error occurred while updating permissions.', {
|
throw new Error('An error occurred while updating permissions.', {
|
||||||
cause: error,
|
cause: error,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ import { css } from '@/styled-system/css'
|
|||||||
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||||
import { LocalUserChoices } from '@/stores/userChoices'
|
import { LocalUserChoices } from '@/stores/userChoices'
|
||||||
import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert'
|
import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert'
|
||||||
import {
|
import { usePostHog } from 'posthog-js/react'
|
||||||
captureEvent,
|
|
||||||
reportError,
|
|
||||||
captureMediaEvent,
|
|
||||||
} from '@/features/analytics/telemetry'
|
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { isFireFox } from '@/utils/livekit'
|
import { isFireFox } from '@/utils/livekit'
|
||||||
import { useIsMobile } from '@/utils/useIsMobile'
|
import { useIsMobile } from '@/utils/useIsMobile'
|
||||||
@@ -41,10 +37,6 @@ import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
|||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||||
import { userStore } from '@/stores/user'
|
import { userStore } from '@/stores/user'
|
||||||
import {
|
|
||||||
PERMISSION_BY_DEVICE_KIND,
|
|
||||||
notePermissionDeniedFromGum,
|
|
||||||
} from '@/stores/permissions'
|
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -55,6 +47,7 @@ export const Conference = ({
|
|||||||
mode?: 'join' | 'create'
|
mode?: 'join' | 'create'
|
||||||
initialRoomData?: ApiRoom
|
initialRoomData?: ApiRoom
|
||||||
}) => {
|
}) => {
|
||||||
|
const posthog = usePostHog()
|
||||||
const { data: apiConfig } = useConfig()
|
const { data: apiConfig } = useConfig()
|
||||||
|
|
||||||
const { userChoices: userConfig } = usePersistentUserChoices() as {
|
const { userChoices: userConfig } = usePersistentUserChoices() as {
|
||||||
@@ -64,8 +57,8 @@ export const Conference = ({
|
|||||||
const { username } = useSnapshot(userStore)
|
const { username } = useSnapshot(userStore)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
captureEvent('visit-room', { slug: roomId })
|
posthog.capture('visit-room', { slug: roomId })
|
||||||
}, [roomId])
|
}, [roomId, posthog])
|
||||||
const fetchKey = [keys.room, roomId]
|
const fetchKey = [keys.room, roomId]
|
||||||
|
|
||||||
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
|
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
|
||||||
@@ -242,10 +235,7 @@ export const Conference = ({
|
|||||||
backgroundColor: 'primaryDark.50 !important',
|
backgroundColor: 'primaryDark.50 !important',
|
||||||
})}
|
})}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
reportError('livekit_room_error', e, {
|
posthog.captureException(e)
|
||||||
path: 'connect_publish',
|
|
||||||
failure: MediaDeviceFailure.getFailure(e) ?? 'not-a-device-error',
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
onConnected={async () => {
|
onConnected={async () => {
|
||||||
if (!apiConfig) return
|
if (!apiConfig) return
|
||||||
@@ -306,22 +296,8 @@ export const Conference = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMediaDeviceFailure={(e, kind) => {
|
onMediaDeviceFailure={(e, kind) => {
|
||||||
if (!e || !kind) return
|
if (e == MediaDeviceFailure.DeviceInUse && !!kind) {
|
||||||
void captureMediaEvent('media-device-error', {
|
setMediaDeviceError({ error: e, kind })
|
||||||
log_code: 'media_devices_error_event',
|
|
||||||
path: 'connect_publish',
|
|
||||||
failure: e,
|
|
||||||
kind,
|
|
||||||
})
|
|
||||||
switch (e) {
|
|
||||||
case MediaDeviceFailure.DeviceInUse:
|
|
||||||
setMediaDeviceError({ error: e, kind })
|
|
||||||
break
|
|
||||||
case MediaDeviceFailure.PermissionDenied:
|
|
||||||
notePermissionDeniedFromGum(PERMISSION_BY_DEVICE_KIND[kind])
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const InviteDialog = ({ mode }: { mode: 'join' | 'create' }) => {
|
|||||||
const { t } = useTranslation('rooms', { keyPrefix: 'shareDialog' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'shareDialog' })
|
||||||
|
|
||||||
const roomData = useRoomData()
|
const roomData = useRoomData()
|
||||||
const roomUrl = roomData?.slug ? getRouteUrl('room', roomData.slug) : ''
|
const roomUrl = getRouteUrl('room', roomData?.slug)
|
||||||
|
|
||||||
const telephony = useTelephony()
|
const telephony = useTelephony()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,167 +0,0 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import { css } from '@/styled-system/css'
|
|
||||||
import { VStack } from '@/styled-system/jsx'
|
|
||||||
import { H } from '@/primitives/H'
|
|
||||||
import { Field } from '@/primitives/Field'
|
|
||||||
import { Form, Text } from '@/primitives'
|
|
||||||
import { Spinner } from '@/primitives/Spinner'
|
|
||||||
import { keys } from '@/api/queryKeys'
|
|
||||||
import { queryClient } from '@/api/queryClient'
|
|
||||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
|
||||||
import { useUser } from '@/features/auth/api/useUser'
|
|
||||||
import { useConfig } from '@/api/useConfig'
|
|
||||||
import { saveUsername, userStore } from '@/stores/user'
|
|
||||||
import { fetchRoom } from '../api/fetchRoom'
|
|
||||||
import { ApiAccessLevel } from '../api/ApiRoom'
|
|
||||||
import { ApiLobbyStatus, type ApiRequestEntry } from '../api/requestEntry'
|
|
||||||
import { useLobby } from '../hooks/useLobby'
|
|
||||||
|
|
||||||
export const Lobby = ({
|
|
||||||
roomId,
|
|
||||||
enterRoom,
|
|
||||||
}: {
|
|
||||||
roomId: string
|
|
||||||
enterRoom: () => void
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
|
||||||
|
|
||||||
const { data: configData } = useConfig()
|
|
||||||
const { isLoggedIn, user } = useUser()
|
|
||||||
const { username } = useSnapshot(userStore)
|
|
||||||
|
|
||||||
// Room data strategy:
|
|
||||||
// 1. Initial fetch is performed to check access and get LiveKit configuration
|
|
||||||
// 2. Data remains valid for 6 hours to avoid unnecessary refetches
|
|
||||||
// 3. State is manually updated via queryClient when a waiting participant is accepted
|
|
||||||
// 4. No automatic refetching or revalidation occurs during this period
|
|
||||||
const {
|
|
||||||
data: roomData,
|
|
||||||
error,
|
|
||||||
isError,
|
|
||||||
refetch: refetchRoom,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: [keys.room, roomId],
|
|
||||||
queryFn: () => fetchRoom({ roomId, username: username || user?.full_name }),
|
|
||||||
staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation
|
|
||||||
retry: false,
|
|
||||||
enabled: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isError && error?.statusCode == 404) {
|
|
||||||
// The room component will handle the room creation if the user is authenticated
|
|
||||||
enterRoom()
|
|
||||||
}
|
|
||||||
}, [isError, error, enterRoom])
|
|
||||||
|
|
||||||
const handleAccepted = (response: ApiRequestEntry) => {
|
|
||||||
queryClient.setQueryData([keys.room, roomId], {
|
|
||||||
...roomData,
|
|
||||||
livekit: response.livekit,
|
|
||||||
})
|
|
||||||
enterRoom()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, startWaiting } = useLobby({
|
|
||||||
roomId,
|
|
||||||
username: username || user?.full_name || 'anonymous',
|
|
||||||
onAccepted: handleAccepted,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { openLoginHint } = useLoginHint()
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const { data } = await refetchRoom()
|
|
||||||
|
|
||||||
if (!data?.livekit) {
|
|
||||||
// Display a message to inform the user that by logging in, they won't have to wait for room entry approval.
|
|
||||||
if (data?.access_level == ApiAccessLevel.TRUSTED) {
|
|
||||||
openLoginHint()
|
|
||||||
}
|
|
||||||
startWaiting()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enterRoom()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case ApiLobbyStatus.TIMEOUT:
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" textAlign="center">
|
|
||||||
<H lvl={1} margin={false} centered>
|
|
||||||
{t('timeoutInvite.title')}
|
|
||||||
</H>
|
|
||||||
<Text as="p" variant="note">
|
|
||||||
{t('timeoutInvite.body')}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
|
|
||||||
case ApiLobbyStatus.DENIED:
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" textAlign="center">
|
|
||||||
<H lvl={1} margin={false} centered>
|
|
||||||
{t('denied.title')}
|
|
||||||
</H>
|
|
||||||
<Text as="p" variant="note">
|
|
||||||
{t('denied.body')}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
|
|
||||||
case ApiLobbyStatus.WAITING:
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" textAlign="center">
|
|
||||||
<H lvl={1} margin={false} centered>
|
|
||||||
{t('waiting.title')}
|
|
||||||
</H>
|
|
||||||
<Text
|
|
||||||
as="p"
|
|
||||||
variant="note"
|
|
||||||
className={css({ marginBottom: '1.5rem' })}
|
|
||||||
>
|
|
||||||
{t('waiting.body')}
|
|
||||||
</Text>
|
|
||||||
<Spinner />
|
|
||||||
</VStack>
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
submitLabel={t('joinLabel')}
|
|
||||||
submitButtonProps={{
|
|
||||||
fullWidth: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VStack marginBottom={1}>
|
|
||||||
<H lvl={1} margin="sm" centered>
|
|
||||||
{t('heading')}
|
|
||||||
</H>
|
|
||||||
{(!isLoggedIn ||
|
|
||||||
configData?.authenticated_users_can_edit_display_name) && (
|
|
||||||
<Field
|
|
||||||
type="text"
|
|
||||||
onChange={saveUsername}
|
|
||||||
label={t('usernameLabel')}
|
|
||||||
id="input-name"
|
|
||||||
defaultValue={username || user?.full_name}
|
|
||||||
validate={(value) => !value && t('errors.usernameEmpty')}
|
|
||||||
wrapperProps={{
|
|
||||||
noMargin: true,
|
|
||||||
fullWidth: true,
|
|
||||||
}}
|
|
||||||
autoComplete="name"
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Button, H, Text, TextArea } from '@/primitives'
|
import { Button, H, Input, Text, TextArea } from '@/primitives'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { cva } from '@/styled-system/css'
|
import { cva } from '@/styled-system/css'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { styled, VStack } from '@/styled-system/jsx'
|
import { styled, VStack } from '@/styled-system/jsx'
|
||||||
|
import { usePostHog } from 'posthog-js/react'
|
||||||
|
import type { PostHog } from 'posthog-js'
|
||||||
import { Button as RACButton } from 'react-aria-components'
|
import { Button as RACButton } from 'react-aria-components'
|
||||||
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
|
||||||
import type { CandidateInfo } from '@/stores/connectionObserver'
|
import type { CandidateInfo } from '@/stores/connectionObserver'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
const Card = styled('div', {
|
const Card = styled('div', {
|
||||||
base: {
|
base: {
|
||||||
@@ -71,9 +72,11 @@ const labelRecipe = cva({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const OpenFeedback = ({
|
const OpenFeedback = ({
|
||||||
|
posthog,
|
||||||
onNext,
|
onNext,
|
||||||
metadata,
|
metadata,
|
||||||
}: {
|
}: {
|
||||||
|
posthog: PostHog
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}) => {
|
}) => {
|
||||||
@@ -87,7 +90,7 @@ const OpenFeedback = ({
|
|||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
try {
|
try {
|
||||||
captureEvent('open-feedback', {
|
posthog.capture('open-feedback', {
|
||||||
feedback,
|
feedback,
|
||||||
...metadata,
|
...metadata,
|
||||||
})
|
})
|
||||||
@@ -138,10 +141,12 @@ const OpenFeedback = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RateQuality = ({
|
const RateQuality = ({
|
||||||
|
posthog,
|
||||||
onNext,
|
onNext,
|
||||||
metadata,
|
metadata,
|
||||||
maxRating = 5,
|
maxRating = 5,
|
||||||
}: {
|
}: {
|
||||||
|
posthog: PostHog
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
maxRating?: number
|
maxRating?: number
|
||||||
@@ -155,7 +160,7 @@ const RateQuality = ({
|
|||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
try {
|
try {
|
||||||
captureEvent('quality-rating', {
|
posthog.capture('quality-rating', {
|
||||||
rating: selectedRating,
|
rating: selectedRating,
|
||||||
...metadata,
|
...metadata,
|
||||||
})
|
})
|
||||||
@@ -238,6 +243,67 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AuthenticationMessage = ({
|
||||||
|
onNext,
|
||||||
|
posthog,
|
||||||
|
}: {
|
||||||
|
onNext: () => void
|
||||||
|
posthog: PostHog
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'authenticationMessage' })
|
||||||
|
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
posthog.people.set({ unsafe_email: email })
|
||||||
|
onNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
maxWidth: '380px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<H lvl={3}>{t('heading')}</H>
|
||||||
|
<Input
|
||||||
|
id="emailInput"
|
||||||
|
name="email"
|
||||||
|
placeholder={t('placeholder')}
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
style={{
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<VStack gap="0.5">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
isDisabled={!email}
|
||||||
|
onPress={onSubmit}
|
||||||
|
>
|
||||||
|
{t('submit')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
invisible
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
fullWidth
|
||||||
|
onPress={onNext}
|
||||||
|
>
|
||||||
|
{t('ignore')}
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type RatingMetadata = {
|
type RatingMetadata = {
|
||||||
room_id?: string
|
room_id?: string
|
||||||
pc_publisher?: CandidateInfo
|
pc_publisher?: CandidateInfo
|
||||||
@@ -252,6 +318,12 @@ export const Rating = ({
|
|||||||
metadata: RatingMetadata
|
metadata: RatingMetadata
|
||||||
}) => {
|
}) => {
|
||||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||||
|
const posthog = usePostHog()
|
||||||
|
|
||||||
|
const isUserAnonymous = useMemo(() => {
|
||||||
|
return posthog.get_property('$user_state') == 'anonymous'
|
||||||
|
}, [posthog])
|
||||||
|
|
||||||
const [step, setStep] = useState(0)
|
const [step, setStep] = useState(0)
|
||||||
|
|
||||||
const sessionId = useMemo(() => crypto.randomUUID(), [])
|
const sessionId = useMemo(() => crypto.randomUUID(), [])
|
||||||
@@ -267,14 +339,37 @@ export const Rating = ({
|
|||||||
if (!isAnalyticsEnabled) return
|
if (!isAnalyticsEnabled) return
|
||||||
|
|
||||||
if (step == 0) {
|
if (step == 0) {
|
||||||
return <RateQuality onNext={() => setStep(step + 1)} metadata={metadata} />
|
return (
|
||||||
|
<RateQuality
|
||||||
|
posthog={posthog}
|
||||||
|
onNext={() => setStep(step + 1)}
|
||||||
|
metadata={metadata}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step == 1) {
|
if (step == 1) {
|
||||||
return <OpenFeedback onNext={() => setStep(step + 1)} metadata={metadata} />
|
return (
|
||||||
|
<OpenFeedback
|
||||||
|
posthog={posthog}
|
||||||
|
onNext={() => setStep(step + 1)}
|
||||||
|
metadata={metadata}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step == 2) {
|
if (step == 2) {
|
||||||
|
return isUserAnonymous ? (
|
||||||
|
<AuthenticationMessage
|
||||||
|
posthog={posthog}
|
||||||
|
onNext={() => setStep(step + 1)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ConfirmationMessage onNext={() => setStep(0)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step == 3) {
|
||||||
return <ConfirmationMessage onNext={() => setStep(0)} />
|
return <ConfirmationMessage onNext={() => setStep(0)} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,160 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { syncPermissions } from '@/stores/permissions'
|
import { permissionsStore } from '@/stores/permissions'
|
||||||
|
import { isSafari } from '@/utils/livekit'
|
||||||
|
|
||||||
|
const POLLING_TIME = 500
|
||||||
|
|
||||||
export const useWatchPermissions = () => {
|
export const useWatchPermissions = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sync = () => void syncPermissions()
|
let cleanup: (() => void) | undefined
|
||||||
sync()
|
let intervalId: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let isCancelled = false
|
||||||
|
|
||||||
navigator.mediaDevices?.addEventListener?.('devicechange', sync)
|
const checkPermissions = async () => {
|
||||||
window.addEventListener('focus', sync)
|
try {
|
||||||
|
if (!navigator.permissions) {
|
||||||
|
if (!isCancelled) {
|
||||||
|
permissionsStore.cameraPermission = 'unavailable'
|
||||||
|
permissionsStore.microphonePermission = 'unavailable'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let statuses: PermissionStatus[] = []
|
const [cameraPermission, microphonePermission] = await Promise.all([
|
||||||
let cancelled = false
|
navigator.permissions.query({ name: 'camera' }),
|
||||||
if (navigator.permissions) {
|
navigator.permissions.query({ name: 'microphone' }),
|
||||||
Promise.all([
|
])
|
||||||
navigator.permissions.query({ name: 'camera' as PermissionName }),
|
|
||||||
navigator.permissions.query({ name: 'microphone' as PermissionName }),
|
if (isCancelled) return
|
||||||
])
|
|
||||||
.then((results) => {
|
/**
|
||||||
if (cancelled) return
|
* Safari Permission API Limitation Workaround
|
||||||
statuses = results
|
*
|
||||||
statuses.forEach((s) => s.addEventListener('change', sync))
|
* Safari has a known issue where permission change events are not reliably fired
|
||||||
})
|
* when users interact with permission prompts. This is documented in Apple's forums:
|
||||||
.catch(() => {
|
* https://developer.apple.com/forums/thread/757353
|
||||||
// Query unsupported: devicechange/focus + gUM outcomes cover it.
|
*
|
||||||
})
|
* The problem:
|
||||||
|
* - When permissions are in 'prompt' state, Safari may not trigger 'change' events
|
||||||
|
* - Users can grant/deny permissions through system prompts, but our listeners won't detect it
|
||||||
|
* - This leaves the UI in an inconsistent state showing outdated permission status
|
||||||
|
*
|
||||||
|
* The solution:
|
||||||
|
* - Manually poll the Permissions API every 500ms when either permission is in 'prompt' state
|
||||||
|
* - Continue polling until both permissions are no longer in 'prompt' state
|
||||||
|
* - This ensures we catch permission changes even when Safari fails to fire events
|
||||||
|
*
|
||||||
|
* This polling is Safari-specific and only activates when needed to minimize performance impact.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
isSafari() &&
|
||||||
|
(cameraPermission.state === 'prompt' ||
|
||||||
|
microphonePermission.state === 'prompt')
|
||||||
|
) {
|
||||||
|
// Start polling every 1 second if either permission is in 'prompt' state
|
||||||
|
if (!intervalId) {
|
||||||
|
intervalId = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const [updatedCamera, updatedMicrophone] = await Promise.all([
|
||||||
|
navigator.permissions.query({ name: 'camera' }),
|
||||||
|
navigator.permissions.query({ name: 'microphone' }),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (isCancelled) return
|
||||||
|
|
||||||
|
const cameraChanged =
|
||||||
|
permissionsStore.cameraPermission !== updatedCamera.state
|
||||||
|
const microphoneChanged =
|
||||||
|
permissionsStore.microphonePermission !==
|
||||||
|
updatedMicrophone.state
|
||||||
|
|
||||||
|
if (cameraChanged) {
|
||||||
|
permissionsStore.cameraPermission = updatedCamera.state
|
||||||
|
}
|
||||||
|
|
||||||
|
if (microphoneChanged) {
|
||||||
|
permissionsStore.microphonePermission =
|
||||||
|
updatedMicrophone.state
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedCamera.state !== 'prompt' &&
|
||||||
|
updatedMicrophone.state !== 'prompt'
|
||||||
|
) {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!isCancelled) {
|
||||||
|
console.error('Error polling permissions:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, POLLING_TIME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
permissionsStore.cameraPermission = cameraPermission.state
|
||||||
|
permissionsStore.microphonePermission = microphonePermission.state
|
||||||
|
|
||||||
|
const handleCameraChange = (e: Event) => {
|
||||||
|
const target = e.target as PermissionStatus
|
||||||
|
permissionsStore.cameraPermission = target.state
|
||||||
|
|
||||||
|
if (
|
||||||
|
intervalId &&
|
||||||
|
target.state !== 'prompt' &&
|
||||||
|
microphonePermission.state !== 'prompt'
|
||||||
|
) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMicrophoneChange = (e: Event) => {
|
||||||
|
const target = e.target as PermissionStatus
|
||||||
|
permissionsStore.microphonePermission = target.state
|
||||||
|
|
||||||
|
if (
|
||||||
|
intervalId &&
|
||||||
|
target.state !== 'prompt' &&
|
||||||
|
microphonePermission.state !== 'prompt'
|
||||||
|
) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cameraPermission.addEventListener('change', handleCameraChange)
|
||||||
|
microphonePermission.addEventListener('change', handleMicrophoneChange)
|
||||||
|
|
||||||
|
cleanup = () => {
|
||||||
|
cameraPermission.removeEventListener('change', handleCameraChange)
|
||||||
|
microphonePermission.removeEventListener(
|
||||||
|
'change',
|
||||||
|
handleMicrophoneChange
|
||||||
|
)
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId)
|
||||||
|
intervalId = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!isCancelled) {
|
||||||
|
console.error('Error checking permissions:', error)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!isCancelled) {
|
||||||
|
permissionsStore.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
checkPermissions()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
isCancelled = true
|
||||||
navigator.mediaDevices?.removeEventListener?.('devicechange', sync)
|
cleanup?.()
|
||||||
window.removeEventListener('focus', sync)
|
|
||||||
statuses.forEach((s) => s.removeEventListener('change', sync))
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { usePermissionsManager } from '../hooks/usePermissionsManager'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { closeSidePanel } from '@/stores/layout'
|
import { closeSidePanel } from '@/stores/layout'
|
||||||
import { useIsAdminOrOwner } from '../hooks/useIsAdminOrOwner'
|
import { useIsAdminOrOwner } from '../hooks/useIsAdminOrOwner'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const Admin = () => {
|
export const Admin = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'admin' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'admin' })
|
||||||
@@ -206,7 +205,7 @@ export const Admin = () => {
|
|||||||
patchRoom({
|
patchRoom({
|
||||||
roomId,
|
roomId,
|
||||||
room: { access_level: value as ApiAccessLevel },
|
room: { access_level: value as ApiAccessLevel },
|
||||||
}).catch((e) => reportError('generic_failure', e))
|
}).catch((e) => console.error(e))
|
||||||
}
|
}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { DisconnectReason, RoomEvent } from 'livekit-client'
|
|||||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||||
|
|
||||||
|
import posthog from 'posthog-js'
|
||||||
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
import { useFeatureFlagEnabled } from 'posthog-js/react'
|
||||||
import { isMobileBrowser } from '@livekit/components-core'
|
import { isMobileBrowser } from '@livekit/components-core'
|
||||||
import { FeatureFlags } from '@/features/analytics/enums'
|
import { FeatureFlags } from '@/features/analytics/enums'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
const CANDIDATE_POLL_INTERVAL_MS = 5000
|
const CANDIDATE_POLL_INTERVAL_MS = 5000
|
||||||
|
|
||||||
@@ -182,23 +182,23 @@ export const ConnectionObserver = () => {
|
|||||||
// total session duration from first connect to final disconnect.
|
// total session duration from first connect to final disconnect.
|
||||||
if (connectionStartTimeRef.current != null) return
|
if (connectionStartTimeRef.current != null) return
|
||||||
connectionStartTimeRef.current = Date.now()
|
connectionStartTimeRef.current = Date.now()
|
||||||
captureEvent('connection-event')
|
posthog.capture('connection-event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReconnect = () => {
|
const handleReconnect = () => {
|
||||||
captureEvent('reconnect-event')
|
posthog.capture('reconnect-event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleReconnected = () => {
|
const handleReconnected = () => {
|
||||||
captureEvent('reconnected-event')
|
posthog.capture('reconnected-event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSignalingConnect = () => {
|
const handleSignalingConnect = () => {
|
||||||
captureEvent('signaling-connect-event')
|
posthog.capture('signaling-connect-event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSignalingReconnect = () => {
|
const handleSignalingReconnect = () => {
|
||||||
captureEvent('signaling-reconnect-event')
|
posthog.capture('signaling-reconnect-event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDisconnect = (
|
const handleDisconnect = (
|
||||||
@@ -206,7 +206,7 @@ export const ConnectionObserver = () => {
|
|||||||
) => {
|
) => {
|
||||||
const connectionEndTime = Date.now()
|
const connectionEndTime = Date.now()
|
||||||
|
|
||||||
captureEvent('disconnect-event', {
|
posthog.capture('disconnect-event', {
|
||||||
// Calculate total session duration from first connection to final disconnect
|
// Calculate total session duration from first connection to final disconnect
|
||||||
// This duration is sensitive to refreshing the page.
|
// This duration is sensitive to refreshing the page.
|
||||||
sessionDuration: connectionStartTimeRef.current
|
sessionDuration: connectionStartTimeRef.current
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const Info = () => {
|
|||||||
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'info' })
|
||||||
|
|
||||||
const data = useRoomData()
|
const data = useRoomData()
|
||||||
const roomUrl = data?.slug ? getRouteUrl('room', data.slug) : ''
|
const roomUrl = getRouteUrl('room', data?.slug)
|
||||||
|
|
||||||
const telephony = useTelephony()
|
const telephony = useTelephony()
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useLocalParticipant } from '@livekit/components-react'
|
|
||||||
import type { LocalTrack } from 'livekit-client'
|
|
||||||
import { useSyncTrackDeviceId } from '../hooks/useSyncTrackDeviceId'
|
|
||||||
import {
|
|
||||||
saveAudioInputDeviceId,
|
|
||||||
saveVideoInputDeviceId,
|
|
||||||
} from '@/stores/userChoices'
|
|
||||||
|
|
||||||
export const SyncDevicePreferences = () => {
|
|
||||||
const { cameraTrack, microphoneTrack } = useLocalParticipant()
|
|
||||||
useSyncTrackDeviceId(
|
|
||||||
cameraTrack?.track as LocalTrack | undefined,
|
|
||||||
saveVideoInputDeviceId
|
|
||||||
)
|
|
||||||
useSyncTrackDeviceId(
|
|
||||||
microphoneTrack?.track as LocalTrack | undefined,
|
|
||||||
saveAudioInputDeviceId
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,4 +1,5 @@
|
|||||||
import type { ProcessorOptions, Track } from 'livekit-client'
|
import type { ProcessorOptions, Track } from 'livekit-client'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
import {
|
import {
|
||||||
FilesetResolver,
|
FilesetResolver,
|
||||||
ImageSegmenter,
|
ImageSegmenter,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
type ProcessorType,
|
type ProcessorType,
|
||||||
MEDIAPIPE_PATH_WASM,
|
MEDIAPIPE_PATH_WASM,
|
||||||
} from '.'
|
} from '.'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry.ts'
|
|
||||||
|
|
||||||
const PROCESSING_WIDTH = 256
|
const PROCESSING_WIDTH = 256
|
||||||
const PROCESSING_HEIGHT = 144
|
const PROCESSING_HEIGHT = 144
|
||||||
@@ -100,7 +100,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
|||||||
await this.initSegmenter()
|
await this.initSegmenter()
|
||||||
this._initWorker()
|
this._initWorker()
|
||||||
|
|
||||||
captureEvent('firefox-blurring-init', {})
|
posthog.capture('firefox-blurring-init')
|
||||||
}
|
}
|
||||||
|
|
||||||
_initVirtualBackgroundImage() {
|
_initVirtualBackgroundImage() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client'
|
import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
import {
|
import {
|
||||||
FilesetResolver,
|
FilesetResolver,
|
||||||
FaceLandmarker,
|
FaceLandmarker,
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
ProcessorType,
|
ProcessorType,
|
||||||
MEDIAPIPE_PATH_WASM,
|
MEDIAPIPE_PATH_WASM,
|
||||||
} from '.'
|
} from '.'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
const PROCESSING_WIDTH = 256 * 3
|
const PROCESSING_WIDTH = 256 * 3
|
||||||
const PROCESSING_HEIGHT = 144 * 3
|
const PROCESSING_HEIGHT = 144 * 3
|
||||||
@@ -101,7 +101,7 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
|
|||||||
await this.initFaceLandmarker()
|
await this.initFaceLandmarker()
|
||||||
this._initWorker()
|
this._initWorker()
|
||||||
|
|
||||||
captureEvent('face-landmarks-init', {})
|
posthog.capture('face-landmarks-init')
|
||||||
}
|
}
|
||||||
|
|
||||||
_initWorker() {
|
_initWorker() {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { ProcessorWrapper } from '@livekit/track-processors'
|
||||||
ProcessorWrapper,
|
|
||||||
supportsBackgroundProcessors,
|
|
||||||
} from '@livekit/track-processors'
|
|
||||||
import type { Track, TrackProcessor } from 'livekit-client'
|
import type { Track, TrackProcessor } from 'livekit-client'
|
||||||
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
|
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
|
||||||
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
|
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
|
||||||
@@ -13,7 +10,7 @@ export const SELFIE_SEGMENTER_MODEL_PATH =
|
|||||||
export const FACE_LANDMARKS_MODEL_PATH =
|
export const FACE_LANDMARKS_MODEL_PATH =
|
||||||
'/assets/mediapipe/models/face_landmarker.task'
|
'/assets/mediapipe/models/face_landmarker.task'
|
||||||
|
|
||||||
export const MEDIAPIPE_PATH_WASM = `/assets/mediapipe/wasm/${__MEDIAPIPE_VERSION__}`
|
export const MEDIAPIPE_PATH_WASM = '/assets/mediapipe/wasm'
|
||||||
|
|
||||||
export enum ProcessorType {
|
export enum ProcessorType {
|
||||||
BLUR = 'blur',
|
BLUR = 'blur',
|
||||||
@@ -37,9 +34,7 @@ export class BackgroundProcessorFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static isSupported() {
|
static isSupported() {
|
||||||
return (
|
return ProcessorWrapper.isSupported || BackgroundCustomProcessor.isSupported
|
||||||
supportsBackgroundProcessors() || BackgroundCustomProcessor.isSupported
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getProcessor(
|
static getProcessor(
|
||||||
@@ -50,7 +45,7 @@ export class BackgroundProcessorFactory {
|
|||||||
|
|
||||||
if (!isBlur && !isVirtual) return undefined
|
if (!isBlur && !isVirtual) return undefined
|
||||||
|
|
||||||
if (supportsBackgroundProcessors()) {
|
if (ProcessorWrapper.isSupported) {
|
||||||
return new UnifiedBackgroundTrackProcessor(config)
|
return new UnifiedBackgroundTrackProcessor(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { RiCameraSwitchLine } from '@remixicon/react'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { ButtonProps } from 'react-aria-components'
|
import type { ButtonProps } from 'react-aria-components'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
enum FacingMode {
|
enum FacingMode {
|
||||||
USER = 'user',
|
USER = 'user',
|
||||||
@@ -104,11 +103,7 @@ export const CameraSwitchButton = (props: Partial<ButtonProps>) => {
|
|||||||
setActiveMediaDevice(device.deviceId)
|
setActiveMediaDevice(device.deviceId)
|
||||||
setFacingMode(target)
|
setFacingMode(target)
|
||||||
} else {
|
} else {
|
||||||
reportError(
|
console.error('Cannot get user device with facingMode ' + target)
|
||||||
'device_switch_failure',
|
|
||||||
new Error('Cannot get user device with facingMode ' + target),
|
|
||||||
{ path: 'switch_device', kind: 'videoinput', facing_mode: target }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
+2
-13
@@ -1,12 +1,8 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
useLocalParticipant,
|
|
||||||
useTrackToggle,
|
|
||||||
UseTrackToggleProps,
|
|
||||||
} from '@livekit/components-react'
|
|
||||||
import { Button, Popover } from '@/primitives'
|
import { Button, Popover } from '@/primitives'
|
||||||
import { RiArrowUpSLine } from '@remixicon/react'
|
import { RiArrowUpSLine } from '@remixicon/react'
|
||||||
import { LocalAudioTrack, Track } from 'livekit-client'
|
import { Track } from 'livekit-client'
|
||||||
|
|
||||||
import { ToggleDevice } from './ToggleDevice'
|
import { ToggleDevice } from './ToggleDevice'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
@@ -55,12 +51,6 @@ export const AudioDevicesControl = ({
|
|||||||
...props,
|
...props,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { microphoneTrack } = useLocalParticipant()
|
|
||||||
const localAudioTrack =
|
|
||||||
microphoneTrack?.track instanceof LocalAudioTrack
|
|
||||||
? microphoneTrack.track
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const kind = 'audioinput'
|
const kind = 'audioinput'
|
||||||
const cannotUseDevice = useCannotUseDevice(kind)
|
const cannotUseDevice = useCannotUseDevice(kind)
|
||||||
const selectLabel = t(`settings.${SettingsDialogExtendedKey.AUDIO}`)
|
const selectLabel = t(`settings.${SettingsDialogExtendedKey.AUDIO}`)
|
||||||
@@ -121,7 +111,6 @@ export const AudioDevicesControl = ({
|
|||||||
context="room"
|
context="room"
|
||||||
kind={kind}
|
kind={kind}
|
||||||
id={audioDeviceId}
|
id={audioDeviceId}
|
||||||
track={localAudioTrack}
|
|
||||||
onSubmit={saveAudioInputDeviceId}
|
onSubmit={saveAudioInputDeviceId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-154
@@ -1,154 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { LocalAudioTrack, TrackEvent } from 'livekit-client'
|
|
||||||
import { useTrackVolume } from '@livekit/components-react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { RiMicLine, RiMicOffLine } from '@remixicon/react'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
import { Text } from '@/primitives'
|
|
||||||
|
|
||||||
const StyledContainer = styled('div', {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.75rem',
|
|
||||||
padding: '0.625rem 0.75rem',
|
|
||||||
marginTop: '0.25rem',
|
|
||||||
borderTop: '1px solid',
|
|
||||||
minHeight: '2.5rem',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
theme: {
|
|
||||||
light: {
|
|
||||||
borderColor: 'greyscale.250',
|
|
||||||
color: 'greyscale.600',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
borderColor: 'rgba(255 255 255 / 0.2)',
|
|
||||||
color: 'rgba(255 255 255 / 0.7)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const StyledGaugeContainer = styled('div', {
|
|
||||||
base: {
|
|
||||||
flexGrow: 1,
|
|
||||||
height: '0.375rem',
|
|
||||||
borderRadius: '0.1875rem',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
theme: {
|
|
||||||
light: {
|
|
||||||
backgroundColor: 'greyscale.250',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
backgroundColor: 'rgba(255 255 255 / 0.25)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const StyledGauge = styled('div', {
|
|
||||||
base: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: 'inherit',
|
|
||||||
transformOrigin: 'left center',
|
|
||||||
transform: 'scaleX(0)',
|
|
||||||
transition: 'transform 0.06s linear',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
theme: {
|
|
||||||
light: {
|
|
||||||
backgroundColor: 'primary.500',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
backgroundColor: 'primaryDark.800',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type Theme = 'light' | 'dark'
|
|
||||||
|
|
||||||
type AudioLevelGaugeProps = {
|
|
||||||
track?: LocalAudioTrack
|
|
||||||
variant?: Theme
|
|
||||||
}
|
|
||||||
|
|
||||||
const useIsTrackMuted = (track?: LocalAudioTrack) => {
|
|
||||||
const [isMuted, setIsMuted] = useState(() => track?.isMuted ?? true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!track) {
|
|
||||||
setIsMuted(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setIsMuted(track.isMuted)
|
|
||||||
const onMuted = () => setIsMuted(true)
|
|
||||||
const onUnmuted = () => setIsMuted(false)
|
|
||||||
track.on(TrackEvent.Muted, onMuted)
|
|
||||||
track.on(TrackEvent.Unmuted, onUnmuted)
|
|
||||||
return () => {
|
|
||||||
track.off(TrackEvent.Muted, onMuted)
|
|
||||||
track.off(TrackEvent.Unmuted, onUnmuted)
|
|
||||||
}
|
|
||||||
}, [track])
|
|
||||||
|
|
||||||
return isMuted
|
|
||||||
}
|
|
||||||
|
|
||||||
const LevelBar = ({
|
|
||||||
track,
|
|
||||||
theme,
|
|
||||||
}: {
|
|
||||||
track: LocalAudioTrack
|
|
||||||
theme: Theme
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
|
||||||
const volume = useTrackVolume(track, {
|
|
||||||
fftSize: 256,
|
|
||||||
smoothingTimeConstant: 0.7,
|
|
||||||
})
|
|
||||||
const level = Math.min(1, volume)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<RiMicLine size={18} aria-hidden="true" />
|
|
||||||
<StyledGaugeContainer
|
|
||||||
theme={theme}
|
|
||||||
role="img"
|
|
||||||
aria-label={t('audioinput.level')}
|
|
||||||
>
|
|
||||||
<StyledGauge theme={theme} style={{ transform: `scaleX(${level})` }} />
|
|
||||||
</StyledGaugeContainer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AudioLevelGauge = ({
|
|
||||||
track,
|
|
||||||
variant = 'light',
|
|
||||||
}: AudioLevelGaugeProps) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
|
||||||
const isMuted = useIsTrackMuted(track)
|
|
||||||
const showMutedHint = !track || isMuted
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer theme={variant}>
|
|
||||||
{showMutedHint ? (
|
|
||||||
<>
|
|
||||||
<RiMicOffLine size={18} aria-hidden="true" />
|
|
||||||
<Text variant="sm">{t('audioinput.muteTest')}</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<LevelBar
|
|
||||||
key={track.mediaStreamTrack?.id}
|
|
||||||
track={track}
|
|
||||||
theme={variant}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</StyledContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-89
@@ -1,89 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { RiVolumeUpLine } from '@remixicon/react'
|
|
||||||
import { styled } from '@/styled-system/jsx'
|
|
||||||
import { Button } from '@/primitives'
|
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput'
|
|
||||||
|
|
||||||
// Speaker test in the audiooutput menu footer (Meet-style UX). Outputs have
|
|
||||||
// no track: the test plays a bundled file through the selected sink, and
|
|
||||||
// following `sinkId` mid-playback re-routes it live. No permission involved.
|
|
||||||
|
|
||||||
type Theme = 'light' | 'dark'
|
|
||||||
|
|
||||||
const BUTTON_VARIANT = {
|
|
||||||
light: 'quaternaryText',
|
|
||||||
dark: 'primaryTextDark',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const StyledContainer = styled('div', {
|
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
paddingTop: '0.5rem',
|
|
||||||
borderTop: '1px solid',
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
theme: {
|
|
||||||
light: {
|
|
||||||
borderColor: 'gray.200',
|
|
||||||
},
|
|
||||||
dark: {
|
|
||||||
borderColor: 'primaryDark.300',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type OutputSoundTesterProps = {
|
|
||||||
/** The device the test should play through (the select's current key). */
|
|
||||||
sinkId?: string
|
|
||||||
variant?: Theme
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OutputSoundTester = ({
|
|
||||||
sinkId,
|
|
||||||
variant = 'light',
|
|
||||||
}: OutputSoundTesterProps) => {
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null)
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sinkId || !canTestAudioOutput()) return
|
|
||||||
audioRef.current?.setSinkId(sinkId).catch((error) => {
|
|
||||||
reportError('device_switch_failure', error, {
|
|
||||||
kind: 'audiooutput',
|
|
||||||
context: 'test sound setSinkId',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [sinkId])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer theme={variant}>
|
|
||||||
<Button
|
|
||||||
variant={BUTTON_VARIANT[variant]}
|
|
||||||
size="sm"
|
|
||||||
fullWidth
|
|
||||||
isDisabled={isPlaying}
|
|
||||||
onPress={() => {
|
|
||||||
audioRef.current
|
|
||||||
?.play()
|
|
||||||
.then(() => setIsPlaying(true))
|
|
||||||
.catch(() => {})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiVolumeUpLine size={18} aria-hidden />
|
|
||||||
{isPlaying ? t('audiooutput.testing') : t('audiooutput.test')}
|
|
||||||
</Button>
|
|
||||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
|
||||||
<audio
|
|
||||||
ref={audioRef}
|
|
||||||
src="sounds/uprise.mp3"
|
|
||||||
onEnded={() => setIsPlaying(false)}
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,6 @@ import { Select, SelectProps } from '@/primitives/Select'
|
|||||||
import type { Placement } from '@react-types/overlays'
|
import type { Placement } from '@react-types/overlays'
|
||||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
import { useDeviceIcons } from '@/features/rooms/livekit/hooks/useDeviceIcons'
|
import { useDeviceIcons } from '@/features/rooms/livekit/hooks/useDeviceIcons'
|
||||||
import type { LocalAudioTrack } from 'livekit-client'
|
|
||||||
import { AudioLevelGauge } from './AudioLevelGauge'
|
|
||||||
import { OutputSoundTester } from './OutputSoundTester'
|
|
||||||
import { canTestAudioOutput } from '@/features/rooms/utils/canTestAudioOutput'
|
|
||||||
|
|
||||||
type DeviceItems = Array<{ value: string; label: string }>
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
@@ -22,7 +18,6 @@ type SelectDeviceProps = {
|
|||||||
onSubmit?: (id: string) => void
|
onSubmit?: (id: string) => void
|
||||||
kind: MediaDeviceKind
|
kind: MediaDeviceKind
|
||||||
context?: 'join' | 'room'
|
context?: 'join' | 'room'
|
||||||
track?: LocalAudioTrack
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectDevicePermissionsProps<T> = SelectDeviceProps &
|
type SelectDevicePermissionsProps<T> = SelectDeviceProps &
|
||||||
@@ -33,7 +28,6 @@ const SelectDevicePermissions = <T extends string | number>({
|
|||||||
kind,
|
kind,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
iconComponent,
|
iconComponent,
|
||||||
track,
|
|
||||||
...props
|
...props
|
||||||
}: SelectDevicePermissionsProps<T>) => {
|
}: SelectDevicePermissionsProps<T>) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
||||||
@@ -80,16 +74,6 @@ const SelectDevicePermissions = <T extends string | number>({
|
|||||||
await setActiveMediaDevice(key as string)
|
await setActiveMediaDevice(key as string)
|
||||||
onSubmit?.(key as string)
|
onSubmit?.(key as string)
|
||||||
}}
|
}}
|
||||||
menuFooter={
|
|
||||||
kind === 'audioinput' ? (
|
|
||||||
<AudioLevelGauge track={track} variant={props.variant} />
|
|
||||||
) : kind === 'audiooutput' && canTestAudioOutput() ? (
|
|
||||||
<OutputSoundTester
|
|
||||||
sinkId={selectedKey as string}
|
|
||||||
variant={props.variant}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -100,7 +84,6 @@ export const SelectDevice = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
kind,
|
kind,
|
||||||
context = 'join',
|
context = 'join',
|
||||||
track,
|
|
||||||
}: SelectDeviceProps) => {
|
}: SelectDeviceProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
||||||
|
|
||||||
@@ -133,7 +116,6 @@ export const SelectDevice = ({
|
|||||||
id={id}
|
id={id}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
track={track}
|
|
||||||
iconComponent={deviceIcons.select}
|
iconComponent={deviceIcons.select}
|
||||||
{...contextProps}
|
{...contextProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-24
@@ -1,7 +1,7 @@
|
|||||||
import { ToggleButton } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||||
import { useMemo, useRef, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
import { appendShortcutLabel } from '@/features/shortcuts/utils'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { PermissionNeededButton } from './PermissionNeededButton'
|
import { PermissionNeededButton } from './PermissionNeededButton'
|
||||||
@@ -16,7 +16,6 @@ import type { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
|||||||
import type { ToggleButtonProps } from '@/primitives/ToggleButton'
|
import type { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||||
import { openPermissionsDialog } from '@/stores/permissions'
|
import { openPermissionsDialog } from '@/stores/permissions'
|
||||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||||
import { requestDevicePermission } from '../../../hooks/useJoinTracks'
|
|
||||||
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||||
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
||||||
import type {
|
import type {
|
||||||
@@ -94,27 +93,6 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
const deviceShortcut = useDeviceShortcut(kind)
|
const deviceShortcut = useDeviceShortcut(kind)
|
||||||
const announce = useScreenReaderAnnounce()
|
const announce = useScreenReaderAnnounce()
|
||||||
|
|
||||||
const isRequestingPermission = useRef(false)
|
|
||||||
|
|
||||||
const onPress = async () => {
|
|
||||||
if (!cannotUseDevice) {
|
|
||||||
toggle()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isRequestingPermission.current) return
|
|
||||||
isRequestingPermission.current = true
|
|
||||||
try {
|
|
||||||
const granted = await requestDevicePermission(kind)
|
|
||||||
if (granted) {
|
|
||||||
toggle()
|
|
||||||
} else {
|
|
||||||
openPermissionsDialog(kind)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isRequestingPermission.current = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useRegisterKeyboardShortcut({
|
useRegisterKeyboardShortcut({
|
||||||
id: deviceShortcut?.id,
|
id: deviceShortcut?.id,
|
||||||
handler: async () => {
|
handler: async () => {
|
||||||
@@ -169,7 +147,12 @@ export const ToggleDevice = <T extends ToggleSource>({
|
|||||||
isDisabled || cannotUseDevice || !enabled ? errorVariant : variant
|
isDisabled || cannotUseDevice || !enabled ? errorVariant : variant
|
||||||
}
|
}
|
||||||
shySelected
|
shySelected
|
||||||
onPress={onPress}
|
onPress={() => {
|
||||||
|
if (cannotUseDevice) {
|
||||||
|
openPermissionsDialog(kind)
|
||||||
|
}
|
||||||
|
toggle()
|
||||||
|
}}
|
||||||
aria-label={toggleLabel}
|
aria-label={toggleLabel}
|
||||||
tooltip={
|
tooltip={
|
||||||
cannotUseDevice
|
cannotUseDevice
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Button } from '@/primitives'
|
|||||||
import { RiPhoneFill } from '@remixicon/react'
|
import { RiPhoneFill } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ConnectionState } from 'livekit-client'
|
import { ConnectionState } from 'livekit-client'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const LeaveButton = () => {
|
export const LeaveButton = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls' })
|
||||||
@@ -16,11 +15,11 @@ export const LeaveButton = () => {
|
|||||||
tooltip={t('leave')}
|
tooltip={t('leave')}
|
||||||
aria-label={t('leave')}
|
aria-label={t('leave')}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
room.disconnect(true).catch((e) =>
|
room
|
||||||
reportError('disconnect_failure', e, {
|
.disconnect(true)
|
||||||
context: 'An error occurred while disconnecting:',
|
.catch((e) =>
|
||||||
})
|
console.error('An error occurred while disconnecting:', e)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
data-attr="controls-leave"
|
data-attr="controls-leave"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { useConfig } from '@/api/useConfig.ts'
|
|||||||
import { proxy, useSnapshot } from 'valtio'
|
import { proxy, useSnapshot } from 'valtio'
|
||||||
import { Spinner } from '@/primitives/Spinner.tsx'
|
import { Spinner } from '@/primitives/Spinner.tsx'
|
||||||
import { userChoicesStore, saveProcessorConfig } from '@/stores/userChoices'
|
import { userChoicesStore, saveProcessorConfig } from '@/stores/userChoices'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
enum BlurRadius {
|
enum BlurRadius {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
@@ -239,9 +238,7 @@ export const EffectsConfiguration = ({
|
|||||||
|
|
||||||
updateEffectStatusMessage(config, wasSelectedBeforeToggle)
|
updateEffectStatusMessage(config, wasSelectedBeforeToggle)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('effects_processor_failure', error, {
|
console.error('Error applying effect:', error)
|
||||||
context: 'Error applying effect:',
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
// Without setTimeout the DOM is not refreshing when updating the options.
|
// Without setTimeout the DOM is not refreshing when updating the options.
|
||||||
setTimeout(() => setProcessorPending(false))
|
setTimeout(() => setProcessorPending(false))
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { RiGlassesLine, RiGoblet2Fill } from '@remixicon/react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor'
|
import { FaceLandmarksProcessor } from '../blur/FaceLandmarksProcessor'
|
||||||
import type { LocalVideoTrack } from 'livekit-client'
|
import type { LocalVideoTrack } from 'livekit-client'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export type FunnyEffectsProps = {
|
export type FunnyEffectsProps = {
|
||||||
videoTrack: LocalVideoTrack
|
videoTrack: LocalVideoTrack
|
||||||
@@ -56,9 +55,7 @@ export const FunnyEffects = ({
|
|||||||
await videoTrack.setProcessor(newProcessor)
|
await videoTrack.setProcessor(newProcessor)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError('effects_processor_failure', e, {
|
console.error('could not update processor', e)
|
||||||
context: 'could not update processor',
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
onPending(false)
|
onPending(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
import { formatPinCode } from '@/features/rooms/utils/telephony'
|
||||||
import type { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
import type { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
||||||
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
import { getRouteUrl } from '@/navigation/getRouteUrl'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
const COPY_SUCCESS_TIMEOUT = 3000
|
const COPY_SUCCESS_TIMEOUT = 3000
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => {
|
|||||||
await navigator.clipboard.writeText(content)
|
await navigator.clipboard.writeText(content)
|
||||||
setIsCopied(true)
|
setIsCopied(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('clipboard_failure', error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ export const useCopyRoomToClipboard = (room: ApiRoom | undefined) => {
|
|||||||
await navigator.clipboard.writeText(roomUrl)
|
await navigator.clipboard.writeText(roomUrl)
|
||||||
setIsRoomUrlCopied(true)
|
setIsRoomUrlCopied(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('clipboard_failure', error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { type TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
import { type TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export function useFullScreen({
|
export function useFullScreen({
|
||||||
trackRef,
|
trackRef,
|
||||||
@@ -57,9 +56,7 @@ export function useFullScreen({
|
|||||||
await docEl.msRequestFullscreen()
|
await docEl.msRequestFullscreen()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('fullscreen_failure', error, {
|
console.error('Error entering fullscreen:', error)
|
||||||
context: 'Error entering fullscreen:',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +70,7 @@ export function useFullScreen({
|
|||||||
await document.msExitFullscreen()
|
await document.msExitFullscreen()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('fullscreen_failure', error, {
|
console.error('Error exiting fullscreen:', error)
|
||||||
context: 'Error exiting fullscreen:',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { useSnapshot } from 'valtio'
|
|
||||||
import {
|
|
||||||
createLocalAudioTrack,
|
|
||||||
createLocalVideoTrack,
|
|
||||||
type LocalAudioTrack,
|
|
||||||
type LocalVideoTrack,
|
|
||||||
MediaDeviceFailure,
|
|
||||||
TrackEvent,
|
|
||||||
} from 'livekit-client'
|
|
||||||
import { BackgroundProcessorFactory } from '../components/blur'
|
|
||||||
import {
|
|
||||||
notePermissionDeniedFromGum,
|
|
||||||
type PermissionKind,
|
|
||||||
} from '@/stores/permissions'
|
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
import {
|
|
||||||
saveAudioInputDeviceId,
|
|
||||||
saveAudioInputEnabled,
|
|
||||||
saveVideoInputDeviceId,
|
|
||||||
saveVideoInputEnabled,
|
|
||||||
userChoicesStore,
|
|
||||||
} from '@/stores/userChoices'
|
|
||||||
import { useSyncTrackDeviceId } from './useSyncTrackDeviceId'
|
|
||||||
|
|
||||||
const VOICE_AUDIO_CONSTRAINTS = {
|
|
||||||
noiseSuppression: true,
|
|
||||||
echoCancellation: true,
|
|
||||||
autoGainControl: true,
|
|
||||||
voiceIsolation: false,
|
|
||||||
sampleRate: 48000,
|
|
||||||
channelCount: 1,
|
|
||||||
sampleSize: 16,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const PERMISSION_KIND: Record<'audioinput' | 'videoinput', PermissionKind> = {
|
|
||||||
audioinput: 'microphone',
|
|
||||||
videoinput: 'camera',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const onJoinPreviewError = (e: Error, kind?: PermissionKind) => {
|
|
||||||
reportError('join_preview_failure', e, { path: 'join_preview' })
|
|
||||||
if (
|
|
||||||
MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.PermissionDenied
|
|
||||||
) {
|
|
||||||
notePermissionDeniedFromGum(kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module-level: effect dependencies, must be referentially stable.
|
|
||||||
const disableAudio = () => saveAudioInputEnabled(false)
|
|
||||||
const disableVideo = () => saveVideoInputEnabled(false)
|
|
||||||
|
|
||||||
const stopAll = (stream: MediaStream) =>
|
|
||||||
stream.getTracks().forEach((track) => track.stop())
|
|
||||||
|
|
||||||
export const requestDevicePermission = async (
|
|
||||||
kind: 'audioinput' | 'videoinput'
|
|
||||||
): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const track =
|
|
||||||
kind === 'audioinput'
|
|
||||||
? await createLocalAudioTrack()
|
|
||||||
: await createLocalVideoTrack()
|
|
||||||
track.stop()
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
onJoinPreviewError(error as Error, PERMISSION_KIND[kind])
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests camera and microphone once on mount (one combined call → at
|
|
||||||
* most one browser dialog) and releases them immediately. Returns true
|
|
||||||
* once settled; track acquisition must wait for it to avoid a second
|
|
||||||
* dialog.
|
|
||||||
*/
|
|
||||||
function useWarmupPermissions(): boolean {
|
|
||||||
const [done, setDone] = useState(false)
|
|
||||||
const started = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (started.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
started.current = true
|
|
||||||
|
|
||||||
const warmup = async () => {
|
|
||||||
try {
|
|
||||||
stopAll(
|
|
||||||
await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: true,
|
|
||||||
video: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
if (
|
|
||||||
MediaDeviceFailure.getFailure(error as Error) ===
|
|
||||||
MediaDeviceFailure.PermissionDenied
|
|
||||||
) {
|
|
||||||
// Retrying after a dismissal would show a second dialog.
|
|
||||||
onJoinPreviewError(error as Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Combined requests fail atomically (e.g. missing webcam fails the
|
|
||||||
// mic too) — retry per kind; permission is settled, no dialog risk.
|
|
||||||
try {
|
|
||||||
stopAll(await navigator.mediaDevices.getUserMedia({ audio: true }))
|
|
||||||
} catch (e) {
|
|
||||||
onJoinPreviewError(e as Error, 'microphone')
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
stopAll(await navigator.mediaDevices.getUserMedia({ video: true }))
|
|
||||||
} catch (e) {
|
|
||||||
onJoinPreviewError(e as Error, 'camera')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setDone(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
warmup()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return done
|
|
||||||
}
|
|
||||||
|
|
||||||
function useLocalTrack<T extends LocalAudioTrack | LocalVideoTrack>({
|
|
||||||
ready,
|
|
||||||
enabled,
|
|
||||||
create,
|
|
||||||
permissionKind,
|
|
||||||
onFailure,
|
|
||||||
}: {
|
|
||||||
ready: boolean
|
|
||||||
enabled: boolean
|
|
||||||
create: () => Promise<T>
|
|
||||||
permissionKind: PermissionKind
|
|
||||||
onFailure: () => void
|
|
||||||
}): T | null {
|
|
||||||
const [track, setTrack] = useState<T | null>(null)
|
|
||||||
|
|
||||||
// Acquire.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ready || !enabled || track) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let cancelled = false
|
|
||||||
create()
|
|
||||||
.then((newTrack) => {
|
|
||||||
if (cancelled) {
|
|
||||||
newTrack.stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setTrack(newTrack)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
onJoinPreviewError(error as Error, permissionKind)
|
|
||||||
onFailure()
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [ready, enabled, track, create, permissionKind, onFailure])
|
|
||||||
|
|
||||||
// Release on toggle-off so the LED turns off.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!enabled && track) {
|
|
||||||
track.stop()
|
|
||||||
setTrack(null)
|
|
||||||
}
|
|
||||||
}, [enabled, track])
|
|
||||||
|
|
||||||
// Track ended externally (permission revoked, device unplugged):
|
|
||||||
// disable instead of re-acquiring, so no unsolicited dialog.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!track) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const handleEnded = () => {
|
|
||||||
setTrack(null)
|
|
||||||
onFailure()
|
|
||||||
}
|
|
||||||
track.on(TrackEvent.Ended, handleEnded)
|
|
||||||
return () => {
|
|
||||||
track.off(TrackEvent.Ended, handleEnded)
|
|
||||||
}
|
|
||||||
}, [track, onFailure])
|
|
||||||
|
|
||||||
// Release on unmount or replacement.
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
track?.stop()
|
|
||||||
}
|
|
||||||
}, [track])
|
|
||||||
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useJoinTracks(): {
|
|
||||||
audioTrack: LocalAudioTrack | undefined
|
|
||||||
videoTrack: LocalVideoTrack | undefined
|
|
||||||
} {
|
|
||||||
const {
|
|
||||||
audioEnabled,
|
|
||||||
videoEnabled,
|
|
||||||
audioDeviceId,
|
|
||||||
videoDeviceId,
|
|
||||||
processorConfig,
|
|
||||||
} = useSnapshot(userChoicesStore)
|
|
||||||
|
|
||||||
const ready = useWarmupPermissions()
|
|
||||||
|
|
||||||
const createAudio = useCallback(
|
|
||||||
() =>
|
|
||||||
createLocalAudioTrack({
|
|
||||||
deviceId: audioDeviceId,
|
|
||||||
...VOICE_AUDIO_CONSTRAINTS,
|
|
||||||
}),
|
|
||||||
[audioDeviceId]
|
|
||||||
)
|
|
||||||
|
|
||||||
const createVideo = useCallback(
|
|
||||||
() =>
|
|
||||||
createLocalVideoTrack({
|
|
||||||
deviceId: videoDeviceId,
|
|
||||||
processor:
|
|
||||||
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
|
||||||
}),
|
|
||||||
[videoDeviceId, processorConfig]
|
|
||||||
)
|
|
||||||
|
|
||||||
const audioTrack = useLocalTrack({
|
|
||||||
ready,
|
|
||||||
enabled: audioEnabled,
|
|
||||||
create: createAudio,
|
|
||||||
permissionKind: 'microphone',
|
|
||||||
onFailure: disableAudio,
|
|
||||||
})
|
|
||||||
|
|
||||||
const videoTrack = useLocalTrack({
|
|
||||||
ready,
|
|
||||||
enabled: videoEnabled,
|
|
||||||
create: createVideo,
|
|
||||||
permissionKind: 'camera',
|
|
||||||
onFailure: disableVideo,
|
|
||||||
})
|
|
||||||
|
|
||||||
useSyncTrackDeviceId(audioTrack ?? undefined, saveAudioInputDeviceId)
|
|
||||||
useSyncTrackDeviceId(videoTrack ?? undefined, saveVideoInputDeviceId)
|
|
||||||
|
|
||||||
return {
|
|
||||||
audioTrack: audioTrack ?? undefined,
|
|
||||||
videoTrack: videoTrack ?? undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
||||||
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const usePermissionsManager = () => {
|
export const usePermissionsManager = () => {
|
||||||
const { mutateAsync: patchRoom } = usePatchRoom()
|
const { mutateAsync: patchRoom } = usePatchRoom()
|
||||||
@@ -29,9 +28,7 @@ export const usePermissionsManager = () => {
|
|||||||
|
|
||||||
return { configuration: newConfiguration }
|
return { configuration: newConfiguration }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('permissions_api_failure', error, {
|
console.error('Failed to update muting permission:', error)
|
||||||
context: 'Failed to update muting permission:',
|
|
||||||
})
|
|
||||||
return { success: false, error }
|
return { success: false, error }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
NotificationType,
|
NotificationType,
|
||||||
useNotifyParticipants,
|
useNotifyParticipants,
|
||||||
} from '@/features/notifications'
|
} from '@/features/notifications'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export const updatePublishSources = (
|
export const updatePublishSources = (
|
||||||
currentSources: Source[],
|
currentSources: Source[],
|
||||||
@@ -109,9 +108,7 @@ export const usePublishSourcesManager = () => {
|
|||||||
|
|
||||||
return { configuration: newConfiguration }
|
return { configuration: newConfiguration }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('publish_sources_failure', error, {
|
console.error(`Failed to update ${sources}:`, error)
|
||||||
context: `Failed to update ${sources}:`,
|
|
||||||
})
|
|
||||||
return { success: false, error }
|
return { success: false, error }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import { isLocal } from '@/utils/livekit'
|
import { isLocal } from '@/utils/livekit'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useRaiseHand } from '@/features/rooms/api/updateRaiseHand'
|
import { useRaiseHand } from '@/features/rooms/api/updateRaiseHand'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
type useRaisedHandProps = {
|
type useRaisedHandProps = {
|
||||||
participant: Participant
|
participant: Participant
|
||||||
@@ -80,11 +79,8 @@ export function useRaisedHand({ participant }: useRaisedHandProps) {
|
|||||||
try {
|
try {
|
||||||
await raiseHand(!isHandRaised)
|
await raiseHand(!isHandRaised)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError(
|
console.error(
|
||||||
'generic_failure',
|
`Failed to toggle hand: ${e instanceof Error ? e.message : 'Unknown error'}`
|
||||||
new Error(
|
|
||||||
`Failed to toggle hand: ${e instanceof Error ? e.message : 'Unknown error'}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const useResolveInitiallyDefaultDeviceId = <
|
||||||
|
T extends { getDeviceId(): Promise<string | undefined> },
|
||||||
|
>(
|
||||||
|
currentId: string,
|
||||||
|
track: T | undefined,
|
||||||
|
save: (id: string) => void
|
||||||
|
) => {
|
||||||
|
const isInitiated = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentId !== 'default' || !track || isInitiated.current) return
|
||||||
|
const resolveDefaultDeviceId = async () => {
|
||||||
|
const actualDeviceId = await track.getDeviceId()
|
||||||
|
if (actualDeviceId && actualDeviceId !== 'default') {
|
||||||
|
isInitiated.current = true
|
||||||
|
save(actualDeviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolveDefaultDeviceId()
|
||||||
|
}, [currentId, track, save])
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { TrackEvent, type LocalTrack } from 'livekit-client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps the persisted device preference aligned with the device the track
|
|
||||||
* is ACTUALLY using — the track is the ground truth, not the store.
|
|
||||||
*
|
|
||||||
* Syncs on mount and on every TrackEvent.Restarted, which covers all the
|
|
||||||
* moments the underlying device can change on the join screen:
|
|
||||||
* - initial acquisition where LiveKit resolved the 'default' alias
|
|
||||||
* (getDeviceId(normalize=true) resolves it to the concrete id via
|
|
||||||
* livekit's DeviceManager — this replaces the former
|
|
||||||
* useResolveInitiallyDefaultDeviceId, which never fired after init),
|
|
||||||
* - a device switch via setDeviceId,
|
|
||||||
* - an unmute re-acquisition,
|
|
||||||
* - the browser falling back to another device.
|
|
||||||
*/
|
|
||||||
export const useSyncTrackDeviceId = (
|
|
||||||
track: LocalTrack | undefined,
|
|
||||||
save: (deviceId: string) => void
|
|
||||||
) => {
|
|
||||||
useEffect(() => {
|
|
||||||
if (!track) return
|
|
||||||
let cancelled = false
|
|
||||||
const sync = () => {
|
|
||||||
track
|
|
||||||
.getDeviceId()
|
|
||||||
.then((deviceId) => {
|
|
||||||
if (!cancelled && deviceId && deviceId !== 'default') {
|
|
||||||
save(deviceId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// A track without settings (ended, screen share) has no id to sync.
|
|
||||||
})
|
|
||||||
}
|
|
||||||
sync()
|
|
||||||
track.on(TrackEvent.Restarted, sync)
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
track.off(TrackEvent.Restarted, sync)
|
|
||||||
}
|
|
||||||
}, [track, save])
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import { SidePanel } from '../components/SidePanel'
|
|||||||
import { RecordingProvider } from '@/features/recording'
|
import { RecordingProvider } from '@/features/recording'
|
||||||
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
||||||
import { ConnectionObserver } from '../components/ConnectionObserver'
|
import { ConnectionObserver } from '../components/ConnectionObserver'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
import { MediaStateObserver } from '../components/MediaStateObserver'
|
import { MediaStateObserver } from '../components/MediaStateObserver'
|
||||||
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
|
import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer'
|
||||||
import { useRoomPageTitle } from '../hooks/useRoomPageTitle'
|
import { useRoomPageTitle } from '../hooks/useRoomPageTitle'
|
||||||
@@ -26,7 +25,6 @@ import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder
|
|||||||
import { StageLayout } from '@/features/layout/components/StageLayout'
|
import { StageLayout } from '@/features/layout/components/StageLayout'
|
||||||
import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer'
|
import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer'
|
||||||
import { ChatProvider } from '@/features/chat/components/ChatProvider'
|
import { ChatProvider } from '@/features/chat/components/ChatProvider'
|
||||||
import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@@ -66,7 +64,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
<>
|
<>
|
||||||
<RoomMetadataSynchronizer />
|
<RoomMetadataSynchronizer />
|
||||||
<ConnectionObserver />
|
<ConnectionObserver />
|
||||||
<SyncDevicePreferences />
|
|
||||||
<MediaStateObserver />
|
<MediaStateObserver />
|
||||||
<ChatProvider />
|
<ChatProvider />
|
||||||
<VideoResolutionSubscription />
|
<VideoResolutionSubscription />
|
||||||
@@ -94,10 +91,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
</RoomContentArea>
|
</RoomContentArea>
|
||||||
<ControlBar
|
<ControlBar
|
||||||
onDeviceError={(e) => {
|
onDeviceError={(e) => {
|
||||||
reportError('device_switch_failure', e.error, {
|
console.error(e)
|
||||||
at: 'ControlBar.onDeviceError',
|
|
||||||
source: e.source,
|
|
||||||
})
|
|
||||||
if (
|
if (
|
||||||
e.source == Track.Source.ScreenShare &&
|
e.source == Track.Source.ScreenShare &&
|
||||||
e.error.toString() ==
|
e.error.toString() ==
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export const canTestAudioOutput = () =>
|
|
||||||
typeof HTMLMediaElement !== 'undefined' &&
|
|
||||||
'setSinkId' in HTMLMediaElement.prototype // Safari: no output routing
|
|
||||||
@@ -5,7 +5,6 @@ import { useUser } from '@/features/auth/api/useUser'
|
|||||||
import { Spinner } from '@/primitives/Spinner'
|
import { Spinner } from '@/primitives/Spinner'
|
||||||
import { CallbackIdHandler } from '../utils/CallbackIdHandler'
|
import { CallbackIdHandler } from '../utils/CallbackIdHandler'
|
||||||
import { PopupWindow } from '../utils/PopupWindow'
|
import { PopupWindow } from '../utils/PopupWindow'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
const callbackIdHandler = new CallbackIdHandler()
|
const callbackIdHandler = new CallbackIdHandler()
|
||||||
const popupWindow = new PopupWindow()
|
const popupWindow = new PopupWindow()
|
||||||
@@ -53,9 +52,7 @@ const CreatePopup = () => {
|
|||||||
popupWindow.close()
|
popupWindow.close()
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError('generic_failure', error, {
|
console.error('Failed to create meeting room:', error)
|
||||||
context: 'Failed to create meeting room:',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isLoggedIn && callbackId) {
|
if (isLoggedIn && callbackId) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { usePatchRoom } from '@/features/rooms/api/patchRoom'
|
|||||||
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
|
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
|
||||||
import { updatePublishSources } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
|
import { updatePublishSources } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
|
||||||
import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf'
|
import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
type Source = Track.Source
|
type Source = Track.Source
|
||||||
|
|
||||||
@@ -109,7 +108,7 @@ const SettingsPopup = () => {
|
|||||||
patchRoom({
|
patchRoom({
|
||||||
roomId: roomSlug,
|
roomId: roomSlug,
|
||||||
room: { configuration: newConfiguration },
|
room: { configuration: newConfiguration },
|
||||||
}).catch((e) => reportError('generic_failure', e))
|
}).catch((e) => console.error(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSource = (sources: Source[], enabled: boolean) => {
|
const updateSource = (sources: Source[], enabled: boolean) => {
|
||||||
@@ -330,7 +329,7 @@ const SettingsPopup = () => {
|
|||||||
patchRoom({
|
patchRoom({
|
||||||
roomId: roomSlug,
|
roomId: roomSlug,
|
||||||
room: { access_level: value as ApiAccessLevel },
|
room: { access_level: value as ApiAccessLevel },
|
||||||
}).catch((e) => reportError('generic_failure', e))
|
}).catch((e) => console.error(e))
|
||||||
}
|
}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { authUrl } from '@/features/auth/utils/authUrl'
|
import { authUrl } from '@/features/auth/utils/authUrl'
|
||||||
import { PopupMessageType, CallbackCreationRoomData } from './types'
|
import { PopupMessageType, CallbackCreationRoomData } from './types'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export class PopupWindow {
|
export class PopupWindow {
|
||||||
private sendMessageToManager(
|
private sendMessageToManager(
|
||||||
@@ -10,7 +9,7 @@ export class PopupWindow {
|
|||||||
callback?: () => void
|
callback?: () => void
|
||||||
) {
|
) {
|
||||||
if (!window.opener) {
|
if (!window.opener) {
|
||||||
reportError('generic_failure', new Error('No manager window found'))
|
console.error('No manager window found')
|
||||||
window.close()
|
window.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useRenameParticipant } from '@/features/rooms/api/renameParticipant'
|
|||||||
import { saveUsername } from '@/stores/user'
|
import { saveUsername } from '@/stores/user'
|
||||||
import { logout } from '@/features/auth/utils/logout'
|
import { logout } from '@/features/auth/utils/logout'
|
||||||
import { useConfig } from '@/api/useConfig'
|
import { useConfig } from '@/api/useConfig'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
export type AccountTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||||
Pick<TabPanelProps, 'id'>
|
Pick<TabPanelProps, 'id'>
|
||||||
@@ -36,11 +35,8 @@ export const AccountTab = ({ id, onOpenChange }: AccountTabProps) => {
|
|||||||
saveUsername(name)
|
saveUsername(name)
|
||||||
onOpenChange?.(false) // only close on success
|
onOpenChange?.(false) // only close on success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reportError(
|
console.error(
|
||||||
'generic_failure',
|
`Failed to rename participant: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||||
new Error(
|
|
||||||
`Failed to rename participant: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { SoundTester } from '@/components/SoundTester'
|
import { SoundTester } from '@/components/SoundTester'
|
||||||
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||||
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
|
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
|
||||||
|
import posthog from 'posthog-js'
|
||||||
import { RowWrapper } from './layout/RowWrapper'
|
import { RowWrapper } from './layout/RowWrapper'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import {
|
import {
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
saveNoiseReductionEnabled,
|
saveNoiseReductionEnabled,
|
||||||
userChoicesStore,
|
userChoicesStore,
|
||||||
} from '@/stores/userChoices'
|
} from '@/stores/userChoices'
|
||||||
import { captureEvent } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
|
export type AudioTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||||
Pick<TabPanelProps, 'id'>
|
Pick<TabPanelProps, 'id'>
|
||||||
@@ -128,7 +128,7 @@ export const AudioTab = ({ id }: AudioTabProps) => {
|
|||||||
isSelected={noiseReductionEnabled}
|
isSelected={noiseReductionEnabled}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
saveNoiseReductionEnabled(v)
|
saveNoiseReductionEnabled(v)
|
||||||
if (v) captureEvent('noise-reduction-init')
|
if (v) posthog.capture('noise-reduction-init')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('audio.noiseReduction.label')}
|
{t('audio.noiseReduction.label')}
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Mikrofon auswählen",
|
"choose": "Mikrofon auswählen",
|
||||||
"level": "Mikrofon-Eingangspegel",
|
|
||||||
"muteTest": "Aktivieren Sie Ihr Mikrofon, um es zu testen",
|
|
||||||
"permissionsNeeded": "Mikrofon auswählen – Berechtigung erforderlich",
|
"permissionsNeeded": "Mikrofon auswählen – Berechtigung erforderlich",
|
||||||
"disable": "Mikrofon deaktivieren",
|
"disable": "Mikrofon deaktivieren",
|
||||||
"enable": "Mikrofon aktivieren",
|
"enable": "Mikrofon aktivieren",
|
||||||
@@ -40,9 +38,7 @@
|
|||||||
},
|
},
|
||||||
"audiooutput": {
|
"audiooutput": {
|
||||||
"choose": "Audioausgabe auswählen",
|
"choose": "Audioausgabe auswählen",
|
||||||
"permissionsNeeded": "Audioausgabe auswählen – Berechtigung erforderlich",
|
"permissionsNeeded": "Audioausgabe auswählen – Berechtigung erforderlich"
|
||||||
"test": "Lautsprecher testen",
|
|
||||||
"testing": "Testton wird abgespielt…"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
@@ -330,6 +326,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"faceLandmarks": {
|
"faceLandmarks": {
|
||||||
"title": "Visuelle Effekte",
|
"title": "Visuelle Effekte",
|
||||||
"glasses": {
|
"glasses": {
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Select microphone",
|
"choose": "Select microphone",
|
||||||
"level": "Microphone input level",
|
|
||||||
"muteTest": "Turn on your microphone to test it",
|
|
||||||
"permissionsNeeded": "Select microphone - permission needed",
|
"permissionsNeeded": "Select microphone - permission needed",
|
||||||
"disable": "Disable microphone",
|
"disable": "Disable microphone",
|
||||||
"enable": "Enable microphone",
|
"enable": "Enable microphone",
|
||||||
@@ -40,9 +38,7 @@
|
|||||||
},
|
},
|
||||||
"audiooutput": {
|
"audiooutput": {
|
||||||
"choose": "Select speaker",
|
"choose": "Select speaker",
|
||||||
"permissionsNeeded": "Select speaker - permission needed",
|
"permissionsNeeded": "Select speaker - permission needed"
|
||||||
"test": "Test speakers",
|
|
||||||
"testing": "Playing test sound…"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Choisir le micro",
|
"choose": "Choisir le micro",
|
||||||
"level": "Niveau d'entrée du micro",
|
|
||||||
"muteTest": "Activez votre micro pour le tester",
|
|
||||||
"permissionsNeeded": "Choisir le micro - autorisations nécessaires",
|
"permissionsNeeded": "Choisir le micro - autorisations nécessaires",
|
||||||
"disable": "Désactiver le micro",
|
"disable": "Désactiver le micro",
|
||||||
"enable": "Activer le micro",
|
"enable": "Activer le micro",
|
||||||
@@ -40,9 +38,7 @@
|
|||||||
},
|
},
|
||||||
"audiooutput": {
|
"audiooutput": {
|
||||||
"choose": "Choisir le haut-parleur",
|
"choose": "Choisir le haut-parleur",
|
||||||
"permissionsNeeded": "Choisir le haut-parleur - autorisations nécessaires",
|
"permissionsNeeded": "Choisir le haut-parleur - autorisations nécessaires"
|
||||||
"test": "Tester les haut-parleurs",
|
|
||||||
"testing": "Lecture du son de test…"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Selecteer microfoon",
|
"choose": "Selecteer microfoon",
|
||||||
"level": "Microfoon-ingangsniveau",
|
|
||||||
"muteTest": "Zet je microfoon aan om deze te testen",
|
|
||||||
"permissionsNeeded": "Selecteer microfoon - Toestemming vereist",
|
"permissionsNeeded": "Selecteer microfoon - Toestemming vereist",
|
||||||
"disable": "Microfoon dempen",
|
"disable": "Microfoon dempen",
|
||||||
"enable": "Microfoon dempen opheffen",
|
"enable": "Microfoon dempen opheffen",
|
||||||
@@ -40,9 +38,7 @@
|
|||||||
},
|
},
|
||||||
"audiooutput": {
|
"audiooutput": {
|
||||||
"choose": "Selecteer luidspreker",
|
"choose": "Selecteer luidspreker",
|
||||||
"permissionsNeeded": "Selecteer luidspreker - Toestemming vereist",
|
"permissionsNeeded": "Selecteer luidspreker - Toestemming vereist"
|
||||||
"test": "Luidsprekers testen",
|
|
||||||
"testing": "Testgeluid wordt afgespeeld…"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { type StyledVariantProps } from '@/styled-system/types'
|
|||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { FieldErrors } from './FieldErrors'
|
import { FieldErrors } from './FieldErrors'
|
||||||
import { FieldDescription } from './FieldDescription'
|
import { FieldDescription } from './FieldDescription'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
// styled taken from example at https://react-spectrum.adobe.com/react-aria/Checkbox.html
|
// styled taken from example at https://react-spectrum.adobe.com/react-aria/Checkbox.html
|
||||||
const StyledCheckbox = styled(RACCheckbox, {
|
const StyledCheckbox = styled(RACCheckbox, {
|
||||||
@@ -113,11 +112,8 @@ export const Checkbox = ({
|
|||||||
const descriptionId = useId()
|
const descriptionId = useId()
|
||||||
|
|
||||||
if (isInvalid !== undefined) {
|
if (isInvalid !== undefined) {
|
||||||
reportError(
|
console.error(
|
||||||
'generic_failure',
|
'Checkbox: passing isInvalid is not supported, use the validate prop instead'
|
||||||
new Error(
|
|
||||||
'Checkbox: passing isInvalid is not supported, use the validate prop instead'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export type SelectProps<T> = Omit<
|
|||||||
errors?: ReactNode
|
errors?: ReactNode
|
||||||
placement?: Placement
|
placement?: Placement
|
||||||
variant?: 'light' | 'dark'
|
variant?: 'light' | 'dark'
|
||||||
menuFooter?: ReactNode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Select = <T extends string | number>({
|
export const Select = <T extends string | number>({
|
||||||
@@ -109,7 +108,6 @@ export const Select = <T extends string | number>({
|
|||||||
errors,
|
errors,
|
||||||
placement,
|
placement,
|
||||||
variant = 'light',
|
variant = 'light',
|
||||||
menuFooter,
|
|
||||||
...props
|
...props
|
||||||
}: SelectProps<T>) => {
|
}: SelectProps<T>) => {
|
||||||
const IconComponent = iconComponent
|
const IconComponent = iconComponent
|
||||||
@@ -157,7 +155,6 @@ export const Select = <T extends string | number>({
|
|||||||
</ListBoxItem>
|
</ListBoxItem>
|
||||||
))}
|
))}
|
||||||
</ListBox>
|
</ListBox>
|
||||||
{menuFooter}
|
|
||||||
</Box>
|
</Box>
|
||||||
</StyledPopover>
|
</StyledPopover>
|
||||||
{errors}
|
{errors}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { proxy, subscribe } from 'valtio'
|
import { proxy, subscribe } from 'valtio'
|
||||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||||
import { deserializeToProxyMap } from '@/utils/valtio'
|
import { deserializeToProxyMap } from '@/utils/valtio'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
export type UiFont =
|
export type UiFont =
|
||||||
| 'default'
|
| 'default'
|
||||||
@@ -155,9 +154,10 @@ function getAccessibilityState(): AccessibilityState {
|
|||||||
|
|
||||||
return DEFAULT_STATE
|
return DEFAULT_STATE
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
reportError('generic_failure', error, {
|
console.error(
|
||||||
context: '[AccessibilityStore] Failed to parse stored settings:',
|
'[AccessibilityStore] Failed to parse stored settings:',
|
||||||
})
|
error
|
||||||
|
)
|
||||||
return DEFAULT_STATE
|
return DEFAULT_STATE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { proxyMap } from 'valtio/utils'
|
|||||||
import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio'
|
import { deserializeToProxyMap, serializeProxyMap } from '@/utils/valtio'
|
||||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||||
import { NotificationType } from '@/features/notifications/NotificationType'
|
import { NotificationType } from '@/features/notifications/NotificationType'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
soundNotifications: Map<NotificationType, boolean>
|
soundNotifications: Map<NotificationType, boolean>
|
||||||
@@ -42,9 +41,10 @@ function getNotificationsState(): State {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
reportError('generic_failure', error, {
|
console.error(
|
||||||
context: '[NotificationsStore] Failed to parse stored settings:',
|
'[NotificationsStore] Failed to parse stored settings:',
|
||||||
})
|
error
|
||||||
|
)
|
||||||
return DEFAULT_STATE
|
return DEFAULT_STATE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { proxy } from 'valtio'
|
import { proxy } from 'valtio'
|
||||||
|
import { derive } from 'derive-valtio'
|
||||||
|
|
||||||
type PermissionState = undefined | 'granted' | 'prompt' | 'denied'
|
type PermissionState =
|
||||||
|
| undefined
|
||||||
|
| 'granted'
|
||||||
|
| 'prompt'
|
||||||
|
| 'denied'
|
||||||
|
| 'unavailable'
|
||||||
|
|
||||||
type State = {
|
type BaseState = {
|
||||||
cameraPermission: PermissionState
|
cameraPermission: PermissionState
|
||||||
microphonePermission: PermissionState
|
microphonePermission: PermissionState
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
isPermissionDialogOpen: boolean
|
isPermissionDialogOpen: boolean
|
||||||
requestOrigin?: 'audioinput' | 'videoinput'
|
requestOrigin?: 'audioinput' | 'videoinput'
|
||||||
|
}
|
||||||
|
|
||||||
|
type DerivedState = {
|
||||||
isCameraGranted: boolean
|
isCameraGranted: boolean
|
||||||
isMicrophoneGranted: boolean
|
isMicrophoneGranted: boolean
|
||||||
isCameraDenied: boolean
|
isCameraDenied: boolean
|
||||||
@@ -16,31 +25,34 @@ type State = {
|
|||||||
isMicrophonePrompted: boolean
|
isMicrophonePrompted: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const permissionsStore = proxy<State>({
|
type State = BaseState & DerivedState
|
||||||
|
|
||||||
|
export const permissionsStore = proxy<BaseState>({
|
||||||
cameraPermission: undefined,
|
cameraPermission: undefined,
|
||||||
microphonePermission: undefined,
|
microphonePermission: undefined,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isPermissionDialogOpen: false,
|
isPermissionDialogOpen: false,
|
||||||
requestOrigin: undefined,
|
requestOrigin: undefined,
|
||||||
get isCameraGranted() {
|
}) as State
|
||||||
return this.cameraPermission === 'granted'
|
|
||||||
|
derive(
|
||||||
|
{
|
||||||
|
isCameraGranted: (get) =>
|
||||||
|
get(permissionsStore).cameraPermission == 'granted',
|
||||||
|
isMicrophoneGranted: (get) =>
|
||||||
|
get(permissionsStore).microphonePermission == 'granted',
|
||||||
|
isCameraDenied: (get) => get(permissionsStore).cameraPermission == 'denied',
|
||||||
|
isMicrophoneDenied: (get) =>
|
||||||
|
get(permissionsStore).microphonePermission == 'denied',
|
||||||
|
isCameraPrompted: (get) =>
|
||||||
|
get(permissionsStore).cameraPermission == 'prompt',
|
||||||
|
isMicrophonePrompted: (get) =>
|
||||||
|
get(permissionsStore).microphonePermission == 'prompt',
|
||||||
},
|
},
|
||||||
get isMicrophoneGranted() {
|
{
|
||||||
return this.microphonePermission === 'granted'
|
proxy: permissionsStore,
|
||||||
},
|
}
|
||||||
get isCameraDenied() {
|
)
|
||||||
return this.cameraPermission === 'denied'
|
|
||||||
},
|
|
||||||
get isMicrophoneDenied() {
|
|
||||||
return this.microphonePermission === 'denied'
|
|
||||||
},
|
|
||||||
get isCameraPrompted() {
|
|
||||||
return this.cameraPermission === 'prompt'
|
|
||||||
},
|
|
||||||
get isMicrophonePrompted() {
|
|
||||||
return this.microphonePermission === 'prompt'
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const openPermissionsDialog = (
|
export const openPermissionsDialog = (
|
||||||
requestOrigin?: 'audioinput' | 'videoinput'
|
requestOrigin?: 'audioinput' | 'videoinput'
|
||||||
@@ -52,84 +64,3 @@ export const openPermissionsDialog = (
|
|||||||
export const closePermissionsDialog = () => {
|
export const closePermissionsDialog = () => {
|
||||||
permissionsStore.isPermissionDialogOpen = false
|
permissionsStore.isPermissionDialogOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PermissionKind = 'camera' | 'microphone'
|
|
||||||
|
|
||||||
const KIND_MAP = {
|
|
||||||
camera: 'videoinput',
|
|
||||||
microphone: 'audioinput',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const PERMISSION_BY_DEVICE_KIND: Partial<
|
|
||||||
Record<MediaDeviceKind, PermissionKind>
|
|
||||||
> = {
|
|
||||||
videoinput: 'camera',
|
|
||||||
audioinput: 'microphone',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setPermissions = (
|
|
||||||
p: Partial<Record<PermissionKind, PermissionState>>
|
|
||||||
) => {
|
|
||||||
if (p.camera && p.camera !== permissionsStore.cameraPermission) {
|
|
||||||
permissionsStore.cameraPermission = p.camera
|
|
||||||
}
|
|
||||||
if (p.microphone && p.microphone !== permissionsStore.microphonePermission) {
|
|
||||||
permissionsStore.microphonePermission = p.microphone
|
|
||||||
}
|
|
||||||
permissionsStore.isLoading = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryPermission = async (name: PermissionKind) => {
|
|
||||||
try {
|
|
||||||
const status = await navigator.permissions.query({
|
|
||||||
name: name as PermissionName,
|
|
||||||
})
|
|
||||||
return status.state
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const labelsVisible = async () => {
|
|
||||||
try {
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
|
||||||
return (kind: PermissionKind) =>
|
|
||||||
devices.some((d) => d.kind === KIND_MAP[kind] && !!d.label)
|
|
||||||
} catch {
|
|
||||||
return (_kind: PermissionKind) => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const syncPermissions = async () => {
|
|
||||||
const [camera, microphone] = await Promise.all([
|
|
||||||
queryPermission('camera'),
|
|
||||||
queryPermission('microphone'),
|
|
||||||
])
|
|
||||||
if (camera && microphone) {
|
|
||||||
setPermissions({ camera, microphone })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const granted = await labelsVisible()
|
|
||||||
const resolve = (kind: PermissionKind, queried?: PermissionState) => {
|
|
||||||
if (queried) return queried
|
|
||||||
const current =
|
|
||||||
kind === 'camera'
|
|
||||||
? permissionsStore.cameraPermission
|
|
||||||
: permissionsStore.microphonePermission
|
|
||||||
if (current === 'denied') return 'denied'
|
|
||||||
return granted(kind) ? 'granted' : 'prompt'
|
|
||||||
}
|
|
||||||
setPermissions({
|
|
||||||
camera: resolve('camera', camera),
|
|
||||||
microphone: resolve('microphone', microphone),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notePermissionDeniedFromGum = (kind?: PermissionKind) => {
|
|
||||||
if (kind) {
|
|
||||||
setPermissions({ [kind]: 'denied' })
|
|
||||||
void syncPermissions()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void syncPermissions()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { proxy, subscribe } from 'valtio'
|
import { proxy, subscribe } from 'valtio'
|
||||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
username: string
|
username: string
|
||||||
@@ -19,9 +18,10 @@ function getUserState(): State {
|
|||||||
...parsed,
|
...parsed,
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
reportError('generic_failure', error, {
|
console.error(
|
||||||
context: '[UserPreferencesStore] Failed to parse stored settings:',
|
'[UserPreferencesStore] Failed to parse stored settings:',
|
||||||
})
|
error
|
||||||
|
)
|
||||||
return DEFAULT_STATE
|
return DEFAULT_STATE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { proxy, subscribe } from 'valtio'
|
import { proxy, subscribe } from 'valtio'
|
||||||
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
is_idle_disconnect_modal_enabled: boolean
|
is_idle_disconnect_modal_enabled: boolean
|
||||||
@@ -22,9 +21,10 @@ function getUserPreferencesState(): State {
|
|||||||
...parsed,
|
...parsed,
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
reportError('generic_failure', error, {
|
console.error(
|
||||||
context: '[UserPreferencesStore] Failed to parse stored settings:',
|
'[UserPreferencesStore] Failed to parse stored settings:',
|
||||||
})
|
error
|
||||||
|
)
|
||||||
return DEFAULT_STATE
|
return DEFAULT_STATE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { reportError } from '@/features/analytics/telemetry'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls all functions in the order they were chained with the same arguments.
|
* Calls all functions in the order they were chained with the same arguments.
|
||||||
@@ -26,7 +25,7 @@ export function chain(...callbacks: any[]): (...args: any[]) => void {
|
|||||||
try {
|
try {
|
||||||
callback(...args)
|
callback(...args)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reportError('generic_failure', e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { isMobileBrowser } from '@livekit/components-core'
|
import { isMobileBrowser } from '@livekit/components-core'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export const useIsMobile = () => {
|
export const useIsMobile = () => {
|
||||||
const [isMobile] = useState(() => isMobileBrowser())
|
const [isMobile, setIsMobile] = useState(isMobileBrowser())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setIsMobile(isMobileBrowser())
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return isMobile
|
return isMobile
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
-4
@@ -1,8 +1,4 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
// Version of @mediapipe/tasks-vision, injected at build time (vite.config.ts).
|
|
||||||
declare const __MEDIAPIPE_VERSION__: string
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL: string
|
readonly VITE_API_BASE_URL: string
|
||||||
readonly VITE_APP_TITLE: string
|
readonly VITE_APP_TITLE: string
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
import svgr from 'vite-plugin-svgr'
|
import svgr from 'vite-plugin-svgr'
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
|
||||||
const mediapipeVersion: string = JSON.parse(
|
|
||||||
readFileSync(
|
|
||||||
new URL(
|
|
||||||
'./node_modules/@mediapipe/tasks-vision/package.json',
|
|
||||||
import.meta.url
|
|
||||||
),
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
).version
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd())
|
const env = loadEnv(mode, process.cwd())
|
||||||
return {
|
return {
|
||||||
define: {
|
|
||||||
__MEDIAPIPE_VERSION__: JSON.stringify(mediapipeVersion),
|
|
||||||
},
|
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
svgr({
|
svgr({
|
||||||
@@ -37,7 +23,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
src: 'node_modules/@mediapipe/tasks-vision/wasm/*',
|
src: 'node_modules/@mediapipe/tasks-vision/wasm/*',
|
||||||
dest: `assets/mediapipe/wasm/${mediapipeVersion}`,
|
dest: 'assets/mediapipe/wasm',
|
||||||
rename: { stripBase: 4 },
|
rename: { stripBase: 4 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,11 +73,6 @@ data:
|
|||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /assets/mediapipe/wasm/ {
|
|
||||||
expires 30d;
|
|
||||||
add_header Cache-Control "public, max-age=2592000";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serve static files with caching
|
# Serve static files with caching
|
||||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@html-to/text-cli": "0.6.0",
|
"@html-to/text-cli": "0.6.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mail_mjml",
|
"name": "mail_mjml",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"description": "An util to generate html and text django's templates from mjml templates",
|
"description": "An util to generate html and text django's templates from mjml templates",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./library",
|
"./library",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sdk",
|
"name": "sdk",
|
||||||
"version": "1.25.2",
|
"version": "1.25.1",
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "summary"
|
name = "summary"
|
||||||
version = "1.25.2"
|
version = "1.25.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi[standard]>=0.105.0",
|
"fastapi[standard]>=0.105.0",
|
||||||
"uvicorn>=0.24.0",
|
"uvicorn>=0.24.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user