Compare commits

..

10 Commits

Author SHA1 Message Date
Jacques ROUSSEL fba2d0de5c ♻️(documentation) remove unused environment variables
Yesterday during a deployment, we discovered that these variables are
unused:
POSTGRES_DB
POSTGRES_USER
POSTGRES_PASSWORD
2025-07-31 17:08:37 +02:00
lebaudantoine bdaf4245da 🔖(minor) bump release to 0.1.33
Warmup with WebSocket pre-authentication on FF
2025-07-25 08:50:33 +02:00
lebaudantoine be63993ba2 🩹(frontend) fix connection warmup with WebSocket pre-authentication
Connection warmup wasn't working properly - only works when trying to
establish WebSocket first, then workaround kicks in. Call WebSocket
endpoint without auth info expecting 401 error, but enough to initiate
cache for subsequent WebSocket functionality.

Scope this **dirty** trick to Firefox users only. Haven't figured out
how to detect proxy from JS code simply.

Tested in staging and works on our constrained WiFi.
2025-07-25 08:50:33 +02:00
lebaudantoine 3d245c3bd4 🔖(minor) bump release to 0.1.32
warmup livekit connection.
2025-07-24 14:57:48 +02:00
lebaudantoine 66a36eff73 ♻️(frontend) refactor connection warmup to use LiveKit methods
Replace custom connection warmup implementation with LiveKit's built-in
methods for better reliability and consistency.
2025-07-24 14:49:49 +02:00
lebaudantoine 387bc2e1f4 🐛(frontend) add LiveKit connection warmup for Firefox+proxy fixes
Implement HTTPS prefetch before joining rooms to resolve WebSocket
handshake failures where Firefox+proxy returns HTTP 200 instead of 101.

Reproduced locally with Squid container. No proxy configuration fixes
found - HTTPS warmup is only working workaround. Issue doesn't occur
when signaling server shares webapp domain, making warmup unnecessary.

Use HEAD request to minimize bandwidth.
2025-07-24 14:32:51 +02:00
lebaudantoine d44b45b6aa 🐛(frontend) prevent shortcut handling when key is undefined
Add null check to avoid processing keyboard shortcuts with undefined
key values that could cause errors.

Exception caught in PostHog
2025-07-23 12:06:39 +02:00
lebaudantoine 224b98fd9a 🧪(frontend) capture LiveKit exceptions in PostHog for debugging
Test tracking signaling failures to determine root causes when connection
fails. Will remove if it spams analytics - goal is understanding failure
patterns and reasons.
2025-07-23 12:06:39 +02:00
lebaudantoine 031852d0b1 🔖(minor) bump release to 0.1.31
Fix noise reduction feature flag
2025-07-21 12:00:06 +02:00
lebaudantoine 24915b0485 🐛(frontend) fix wrong feature flag for noise reduction
Replace incorrect faceLandmarks flag with proper noiseReduction flag
to ensure correct feature availability checking.
2025-07-21 10:29:36 +02:00
29 changed files with 222 additions and 396 deletions
-3
View File
@@ -35,9 +35,6 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
LIVEKIT_API_SECRET: secret
-3
View File
@@ -217,9 +217,6 @@ DB_NAME: meet
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
## Deployment
+4
View File
@@ -50,6 +50,10 @@ def get_frontend_configuration(request):
else None,
"default_country": settings.ROOM_TELEPHONY_DEFAULT_COUNTRY,
},
"livekit": {
"url": settings.LIVEKIT_CONFIGURATION["url"],
"enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND,
},
}
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
return Response(frontend_configuration)
+5
View File
@@ -492,6 +492,11 @@ class Base(Configuration):
),
"url": values.Value(environ_name="LIVEKIT_API_URL", environ_prefix=None),
}
LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND = values.BooleanValue(
environ_name="LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND",
environ_prefix=None,
default=False,
)
LIVEKIT_VERIFY_SSL = values.BooleanValue(
True, environ_name="LIVEKIT_VERIFY_SSL", environ_prefix=None
)
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.30"
version = "0.1.33"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+2 -54
View File
@@ -1,19 +1,18 @@
{
"name": "meet",
"version": "0.1.30",
"version": "0.1.33",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.30",
"version": "0.1.33",
"dependencies": {
"@livekit/components-react": "2.9.13",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.5.7",
"@pandacss/preset-panda": "0.54.0",
"@react-aria/toast": "3.0.5",
"@react-hook/size": "2.1.2",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.81.5",
"@timephy/rnnoise-wasm": "1.0.0",
@@ -1211,12 +1210,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0"
},
"node_modules/@livekit/components-core": {
"version": "0.12.8",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.8.tgz",
@@ -2708,51 +2701,6 @@
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-hook/latest": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@react-hook/latest/-/latest-1.0.3.tgz",
"integrity": "sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-hook/passive-layout-effect": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz",
"integrity": "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-hook/resize-observer": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz",
"integrity": "sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA==",
"license": "MIT",
"dependencies": {
"@juggle/resize-observer": "^3.3.1",
"@react-hook/latest": "^1.0.2",
"@react-hook/passive-layout-effect": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-hook/size": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@react-hook/size/-/size-2.1.2.tgz",
"integrity": "sha512-BmE5asyRDxSuQ9p14FUKJ0iBRgV9cROjqNG9jT/EjCM+xHha1HVqbPoT+14FQg1K7xIydabClCibUY4+1tw/iw==",
"license": "MIT",
"dependencies": {
"@react-hook/passive-layout-effect": "^1.2.0",
"@react-hook/resize-observer": "^1.2.1"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/@react-stately/autocomplete": {
"version": "3.0.0-beta.2",
"resolved": "https://registry.npmjs.org/@react-stately/autocomplete/-/autocomplete-3.0.0-beta.2.tgz",
+1 -2
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.30",
"version": "0.1.33",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -18,7 +18,6 @@
"@livekit/track-processors": "0.5.7",
"@pandacss/preset-panda": "0.54.0",
"@react-aria/toast": "3.0.5",
"@react-hook/size": "2.1.2",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.81.5",
"@timephy/rnnoise-wasm": "1.0.0",
+4
View File
@@ -37,6 +37,10 @@ export interface ApiConfig {
default_country?: string
}
manifest_link?: string
livekit: {
url: string
enable_firefox_proxy_workaround: boolean
}
}
const fetchConfig = (): Promise<ApiConfig> => {
@@ -7,8 +7,6 @@ import {
MediaDeviceFailure,
Room,
RoomOptions,
supportsAdaptiveStream,
supportsDynacast,
} from 'livekit-client'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
@@ -20,12 +18,14 @@ import { ApiRoom } from '../api/ApiRoom'
import { useCreateRoom } from '../api/createRoom'
import { InviteDialog } from './InviteDialog'
import { VideoConference } from '../livekit/prefabs/VideoConference'
import posthog from 'posthog-js'
import { css } from '@/styled-system/css'
import { BackgroundProcessorFactory } from '../livekit/components/blur'
import { LocalUserChoices } from '@/stores/userChoices'
import { navigateTo } from '@/navigation/navigateTo'
import { MediaDeviceErrorAlert } from './MediaDeviceErrorAlert'
import { usePostHog } from 'posthog-js/react'
import { useConfig } from '@/api/useConfig'
import { isFireFox } from '@/utils/livekit'
export const Conference = ({
roomId,
@@ -38,11 +38,16 @@ export const Conference = ({
mode?: 'join' | 'create'
initialRoomData?: ApiRoom
}) => {
const posthog = usePostHog()
const { data: apiConfig } = useConfig()
useEffect(() => {
posthog.capture('visit-room', { slug: roomId })
}, [roomId])
}, [roomId, posthog])
const fetchKey = [keys.room, roomId]
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
const {
mutateAsync: createRoom,
status: createStatus,
@@ -76,8 +81,8 @@ export const Conference = ({
const roomOptions = useMemo((): RoomOptions => {
return {
adaptiveStream: supportsAdaptiveStream(),
dynacast: supportsDynacast(),
adaptiveStream: true,
dynacast: true,
publishDefaults: {
videoCodec: 'vp9',
},
@@ -93,6 +98,51 @@ export const Conference = ({
const room = useMemo(() => new Room(roomOptions), [roomOptions])
useEffect(() => {
/**
* Warm up connection to LiveKit server before joining room
* This prefetch helps reduce initial connection latency by establishing
* an early HTTP connection to the WebRTC signaling server
*
* It should cache DNS and TLS keys.
*/
const prepareConnection = async () => {
if (!apiConfig || isConnectionWarmedUp) return
await room.prepareConnection(apiConfig.livekit.url)
if (isFireFox() && apiConfig.livekit.enable_firefox_proxy_workaround) {
try {
const wssUrl =
apiConfig.livekit.url
.replace('https://', 'wss://')
.replace(/\/$/, '') + '/rtc'
/**
* FIREFOX + PROXY WORKAROUND:
*
* Issue: On Firefox behind proxy configurations, WebSocket signaling fails to establish.
* Symptom: Client receives HTTP 200 instead of expected 101 (Switching Protocols).
* Root Cause: Certificate/security issue where the initial request is considered unsecure.
*
* Solution: Pre-establish a WebSocket connection to the signaling server, which fails.
* This "primes" the connection, allowing subsequent WebSocket establishments to work correctly.
*
* Note: This issue is reproducible on LiveKit's demo app.
* Reference: livekit-examples/meet/issues/466
*/
const ws = new WebSocket(wssUrl)
// 401 unauthorized response is expected
ws.onerror = () => ws.readyState <= 1 && ws.close()
} catch (e) {
console.debug('Firefox WebSocket workaround failed.', e)
}
}
setIsConnectionWarmedUp(true)
}
prepareConnection()
}, [room, apiConfig, isConnectionWarmedUp])
const [showInviteDialog, setShowInviteDialog] = useState(mode === 'create')
const [mediaDeviceError, setMediaDeviceError] = useState<{
error: MediaDeviceFailure | null
@@ -125,9 +175,9 @@ export const Conference = ({
<Screen header={false} footer={false}>
<LiveKitRoom
room={room}
serverUrl={data?.livekit?.url}
serverUrl={apiConfig?.livekit.url}
token={data?.livekit?.token}
connect={true}
connect={isConnectionWarmedUp}
audio={userConfig.audioEnabled}
video={
userConfig.videoEnabled && {
@@ -140,6 +190,9 @@ export const Conference = ({
className={css({
backgroundColor: 'primaryDark.50 !important',
})}
onError={(e) => {
posthog.captureException(e)
}}
onDisconnected={(e) => {
if (e == DisconnectReason.CLIENT_INITIATED) {
navigateTo('feedback', { duplicateIdentity: false })
@@ -4,9 +4,9 @@ import { useMemo, useRef } from 'react'
import { ScreenSharePreferenceStore } from '@/stores/ScreenSharePreferences'
import { useSnapshot } from 'valtio'
import { useLocalParticipant } from '@livekit/components-react'
import { useSize } from '../hooks/useResizeObserver'
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { useTranslation } from 'react-i18next'
import useSize from '@react-hook/size'
export const FullScreenShareWarning = ({
trackReference,
@@ -16,7 +16,7 @@ export const FullScreenShareWarning = ({
const { t } = useTranslation('rooms', { keyPrefix: 'fullScreenWarning' })
const warningContainerRef = useRef<HTMLDivElement>(null)
const containerWidth = useSize(warningContainerRef)[0]
const { width: containerWidth } = useSize(warningContainerRef)
const { localParticipant } = useLocalParticipant()
const screenSharePreferences = useSnapshot(ScreenSharePreferenceStore)
@@ -3,8 +3,8 @@ import { styled } from '@/styled-system/jsx'
import { Avatar } from '@/components/Avatar'
import { useIsSpeaking } from '@livekit/components-react'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
import { useMemo, useRef } from 'react'
import useSize from '@react-hook/size'
const StyledParticipantPlaceHolder = styled('div', {
base: {
@@ -28,7 +28,7 @@ export const ParticipantPlaceholder = ({
const participantColor = getParticipantColor(participant)
const placeholderEl = useRef<HTMLDivElement>(null)
const [width, height] = useSize(placeholderEl)
const { width, height } = useSize(placeholderEl)
const minDimension = Math.min(width, height)
const avatarSize = useMemo(
@@ -1,55 +0,0 @@
import * as React from 'react'
import { createInteractingObservable } from '@livekit/components-core'
import { usePagination } from '@livekit/components-react'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
export interface PaginationControlProps
extends Pick<
ReturnType<typeof usePagination>,
'totalPageCount' | 'nextPage' | 'prevPage' | 'currentPage'
> {
/** Reference to an HTML element that holds the pages, while interacting (`mouseover`)
* with it, the pagination controls will appear for a while. */
pagesContainer?: React.RefObject<HTMLElement>
}
export function PaginationControl({
totalPageCount,
nextPage,
prevPage,
currentPage,
pagesContainer: connectedElement,
}: PaginationControlProps) {
const [interactive, setInteractive] = React.useState(false)
React.useEffect(() => {
let subscription:
| ReturnType<ReturnType<typeof createInteractingObservable>['subscribe']>
| undefined
if (connectedElement) {
subscription = createInteractingObservable(
connectedElement.current,
2000
).subscribe(setInteractive)
}
return () => {
if (subscription) {
subscription.unsubscribe()
}
}
}, [connectedElement])
return (
<div
className="lk-pagination-control"
data-lk-user-interaction={interactive}
>
<button className="lk-button" onClick={prevPage}>
<RiArrowLeftSLine />
</button>
<span className="lk-pagination-count">{`${currentPage} of ${totalPageCount}`}</span>
<button className="lk-button" onClick={nextPage}>
<RiArrowRightSLine />
</button>
</div>
)
}
@@ -1,30 +0,0 @@
import * as React from 'react'
export interface PaginationIndicatorProps {
totalPageCount: number
currentPage: number
}
export const PaginationIndicator: (
props: PaginationIndicatorProps & React.RefAttributes<HTMLDivElement>
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
HTMLDivElement,
PaginationIndicatorProps
>(function PaginationIndicator(
{ totalPageCount, currentPage }: PaginationIndicatorProps,
ref
) {
const bubbles = new Array(totalPageCount).fill('').map((_, index) => {
if (index + 1 === currentPage) {
return <span data-lk-active key={index} />
} else {
return <span key={index} />
}
})
return (
<div ref={ref} className="lk-pagination-indicator">
{bubbles}
</div>
)
})
@@ -1,92 +0,0 @@
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { getScrollBarWidth } from '@livekit/components-core'
import * as React from 'react'
import { TrackLoop, useVisualStableUpdate } from '@livekit/components-react'
import useSize from '@react-hook/size'
const MIN_HEIGHT = 130
const MIN_WIDTH = 140
const MIN_VISIBLE_TILES = 1
const ASPECT_RATIO = 16 / 10
const ASPECT_RATIO_INVERT = (1 - ASPECT_RATIO) * -1
/** @public */
export interface CarouselLayoutProps
extends React.HTMLAttributes<HTMLMediaElement> {
tracks: TrackReferenceOrPlaceholder[]
children: React.ReactNode
/** Place the tiles vertically or horizontally next to each other.
* If undefined orientation is guessed by the dimensions of the container. */
orientation?: 'vertical' | 'horizontal'
}
/**
* The `CarouselLayout` component displays a list of tracks in a scroll container.
* It will display as many tiles as possible and overflow the rest.
* @remarks
* To ensure visual stability when tiles are reordered due to track updates,
* the component uses the `useVisualStableUpdate` hook.
* @example
* ```tsx
* const tracks = useTracks([Track.Source.Camera]);
* <CarouselLayout tracks={tracks}>
* <ParticipantTile />
* </CarouselLayout>
* ```
* @public
*/
export function CarouselLayout({
tracks,
orientation,
...props
}: CarouselLayoutProps) {
const asideEl = React.useRef<HTMLDivElement>(null)
const [prevTiles, setPrevTiles] = React.useState(0)
const [width, height] = useSize(asideEl)
const carouselOrientation = orientation
? orientation
: height >= width
? 'vertical'
: 'horizontal'
const tileSpan =
carouselOrientation === 'vertical'
? Math.max(width * ASPECT_RATIO_INVERT, MIN_HEIGHT)
: Math.max(height * ASPECT_RATIO, MIN_WIDTH)
const scrollBarWidth = getScrollBarWidth()
const tilesThatFit =
carouselOrientation === 'vertical'
? Math.max((height - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES)
: Math.max((width - scrollBarWidth) / tileSpan, MIN_VISIBLE_TILES)
let maxVisibleTiles = Math.round(tilesThatFit)
if (Math.abs(tilesThatFit - prevTiles) < 0.5) {
maxVisibleTiles = Math.round(prevTiles)
} else if (prevTiles !== tilesThatFit) {
setPrevTiles(tilesThatFit)
}
const sortedTiles = useVisualStableUpdate(tracks, maxVisibleTiles)
React.useLayoutEffect(() => {
if (asideEl.current) {
asideEl.current.dataset.lkOrientation = carouselOrientation
asideEl.current.style.setProperty(
'--lk-max-visible-tiles',
maxVisibleTiles.toString()
)
}
}, [maxVisibleTiles, carouselOrientation])
return (
<aside
key={carouselOrientation}
className="lk-carousel"
ref={asideEl}
{...props}
>
<TrackLoop tracks={sortedTiles}>{props.children}</TrackLoop>
</aside>
)
}
@@ -1,71 +0,0 @@
import * as React from 'react'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import {
TrackLoop,
usePagination,
UseParticipantsOptions,
useSwipe,
} from '@livekit/components-react'
import { mergeProps } from '@/utils/mergeProps'
import { PaginationIndicator } from '../controls/PaginationIndicator'
import { useGridLayout } from '../../hooks/useGridLayout'
import { PaginationControl } from '../controls/PaginationControl'
/** @public */
export interface GridLayoutProps
extends React.HTMLAttributes<HTMLDivElement>,
Pick<UseParticipantsOptions, 'updateOnlyOn'> {
children: React.ReactNode
tracks: TrackReferenceOrPlaceholder[]
}
/**
* The `GridLayout` component displays the nested participants in a grid where every participants has the same size.
* It also supports pagination if there are more participants than the grid can display.
* @remarks
* To ensure visual stability when tiles are reordered due to track updates,
* the component uses the `useVisualStableUpdate` hook.
* @example
* ```tsx
* <LiveKitRoom>
* <GridLayout tracks={tracks}>
* <ParticipantTile />
* </GridLayout>
* <LiveKitRoom>
* ```
* @public
*/
export function GridLayout({ tracks, ...props }: GridLayoutProps) {
const gridEl = React.createRef<HTMLDivElement>()
const elementProps = React.useMemo(
() => mergeProps(props, { className: 'lk-grid-layout' }),
[props]
)
const { layout } = useGridLayout(gridEl, tracks.length)
const pagination = usePagination(layout.maxTiles, tracks)
useSwipe(gridEl, {
onLeftSwipe: pagination.nextPage,
onRightSwipe: pagination.prevPage,
})
return (
<div
ref={gridEl}
data-lk-pagination={pagination.totalPageCount > 1}
{...elementProps}
>
<TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
{tracks.length > layout.maxTiles && (
<>
<PaginationIndicator
totalPageCount={pagination.totalPageCount}
currentPage={pagination.currentPage}
/>
<PaginationControl pagesContainer={gridEl} {...pagination} />
</>
)}
</div>
)
}
@@ -1,53 +0,0 @@
import { GRID_LAYOUTS, selectGridLayout } from '@livekit/components-core'
import type {
GridLayoutDefinition,
GridLayoutInfo,
} from '@livekit/components-core'
import * as React from 'react'
import useSize from '@react-hook/size'
/**
* The `useGridLayout` hook tries to select the best layout to fit all tiles.
* If the available screen space is not enough, it will reduce the number of maximum visible
* tiles and select a layout that still works visually within the given limitations.
* As the order of tiles changes over time, the hook tries to keep visual updates to a minimum
* while trying to display important tiles such as speaking participants or screen shares.
*
* @example
* ```tsx
* const { layout } = useGridLayout(gridElement, trackCount);
* ```
* @public
*/
export function useGridLayout(
/** HTML element that contains the grid. */
gridElement: React.RefObject<HTMLDivElement>,
/** Count of tracks that should get layed out */
trackCount: number,
options: {
gridLayouts?: GridLayoutDefinition[]
} = {}
): { layout: GridLayoutInfo; containerWidth: number; containerHeight: number } {
const gridLayouts = options.gridLayouts ?? GRID_LAYOUTS
const [width, height] = useSize(gridElement)
const layout = selectGridLayout(gridLayouts, trackCount, width, height)
React.useEffect(() => {
if (gridElement.current && layout) {
gridElement.current.style.setProperty(
'--lk-col-count',
layout?.columns.toString()
)
gridElement.current.style.setProperty(
'--lk-row-count',
layout?.rows.toString()
)
}
}, [gridElement, layout])
return {
layout,
containerWidth: width,
containerHeight: height,
}
}
@@ -4,7 +4,7 @@ import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalytics
import { isMobileBrowser } from '@livekit/components-core'
export const useNoiseReductionAvailable = () => {
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.noiseReduction)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isMobile = isMobileBrowser()
@@ -0,0 +1,127 @@
/* eslint-disable react-hooks/exhaustive-deps */
import * as React from 'react'
const useLatest = <T>(current: T) => {
const storedValue = React.useRef(current)
React.useEffect(() => {
storedValue.current = current
})
return storedValue
}
/**
* A React hook that fires a callback whenever ResizeObserver detects a change to its size
* code extracted from https://github.com/jaredLunde/react-hook/blob/master/packages/resize-observer/src/index.tsx in order to not include the polyfill for resize-observer
*
* @internal
*/
export function useResizeObserver<T extends HTMLElement>(
target: React.RefObject<T>,
callback: UseResizeObserverCallback
) {
const resizeObserver = getResizeObserver()
const storedCallback = useLatest(callback)
React.useLayoutEffect(() => {
let didUnsubscribe = false
const targetEl = target.current
if (!targetEl) return
function cb(entry: ResizeObserverEntry, observer: ResizeObserver) {
if (didUnsubscribe) return
storedCallback.current(entry, observer)
}
resizeObserver?.subscribe(targetEl as HTMLElement, cb)
return () => {
didUnsubscribe = true
resizeObserver?.unsubscribe(targetEl as HTMLElement, cb)
}
}, [target.current, resizeObserver, storedCallback])
return resizeObserver?.observer
}
function createResizeObserver() {
let ticking = false
let allEntries: ResizeObserverEntry[] = []
const callbacks: Map<unknown, Array<UseResizeObserverCallback>> = new Map()
if (typeof window === 'undefined') {
return
}
const observer = new ResizeObserver(
(entries: ResizeObserverEntry[], obs: ResizeObserver) => {
allEntries = allEntries.concat(entries)
if (!ticking) {
window.requestAnimationFrame(() => {
const triggered = new Set<Element>()
for (let i = 0; i < allEntries.length; i++) {
if (triggered.has(allEntries[i].target)) continue
triggered.add(allEntries[i].target)
const cbs = callbacks.get(allEntries[i].target)
cbs?.forEach((cb) => cb(allEntries[i], obs))
}
allEntries = []
ticking = false
})
}
ticking = true
}
)
return {
observer,
subscribe(target: HTMLElement, callback: UseResizeObserverCallback) {
observer.observe(target)
const cbs = callbacks.get(target) ?? []
cbs.push(callback)
callbacks.set(target, cbs)
},
unsubscribe(target: HTMLElement, callback: UseResizeObserverCallback) {
const cbs = callbacks.get(target) ?? []
if (cbs.length === 1) {
observer.unobserve(target)
callbacks.delete(target)
return
}
const cbIndex = cbs.indexOf(callback)
if (cbIndex !== -1) cbs.splice(cbIndex, 1)
callbacks.set(target, cbs)
},
}
}
let _resizeObserver: ReturnType<typeof createResizeObserver>
const getResizeObserver = () =>
!_resizeObserver
? (_resizeObserver = createResizeObserver())
: _resizeObserver
export type UseResizeObserverCallback = (
entry: ResizeObserverEntry,
observer: ResizeObserver
) => unknown
export const useSize = (target: React.RefObject<HTMLDivElement>) => {
const [size, setSize] = React.useState({ width: 0, height: 0 })
React.useLayoutEffect(() => {
if (target.current) {
const { width, height } = target.current.getBoundingClientRect()
setSize({ width, height })
}
}, [target.current])
const resizeCallback = React.useCallback(
(entry: ResizeObserverEntry) => setSize(entry.contentRect),
[]
)
// Where the magic happens
useResizeObserver(target, resizeCallback)
return size
}
@@ -4,13 +4,13 @@ import { ParticipantsToggle } from '../../components/controls/Participants/Parti
import { ToolsToggle } from '../../components/controls/ToolsToggle'
import { InfoToggle } from '../../components/controls/InfoToggle'
import { AdminToggle } from '../../components/AdminToggle'
import { useSize } from '../../hooks/useResizeObserver'
import { useState, RefObject } from 'react'
import { Dialog, DialogTrigger, Popover } from 'react-aria-components'
import { Button } from '@/primitives'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import useSize from '@react-hook/size'
const CONTROL_BAR_BREAKPOINT = 1100
@@ -70,7 +70,7 @@ export const MoreOptions = ({
}: {
parentElement: RefObject<HTMLDivElement>
}) => {
const parentWidth = useSize(parentElement)[0]
const { width: parentWidth } = useSize(parentElement)
return (
<div
className={css({
@@ -9,6 +9,7 @@ import { RoomEvent, Track } from 'livekit-client'
import * as React from 'react'
import { useState } from 'react'
import {
CarouselLayout,
ConnectionStateToast,
FocusLayoutContainer,
GridLayout,
@@ -31,7 +32,6 @@ import { RecordingStateToast } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { useConnectionObserver } from '../hooks/useConnectionObserver'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
import { CarouselLayout } from '../components/layout/CarouselLayout'
const LayoutWrapper = styled(
'div',
@@ -12,6 +12,7 @@ export const useKeyboardShortcuts = () => {
// Issues might occur. First draft.
const onKeyDown = (e: KeyboardEvent) => {
const { key, metaKey, ctrlKey } = e
if (!key) return
const shortcutKey = formatShortcutKey({
key,
ctrlKey: ctrlKey || (isMacintosh() && metaKey),
@@ -40,9 +40,6 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
@@ -40,9 +40,6 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
@@ -52,6 +49,7 @@ backend:
{{- end }}
{{- end }}
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND: True
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
@@ -62,9 +62,6 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
{{- with .Values.livekit.keys }}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.30",
"version": "0.1.33",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.30",
"version": "0.1.33",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.30",
"version": "0.1.33",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "0.1.30",
"version": "0.1.33",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.30",
"version": "0.1.33",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.30",
"version": "0.1.33",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "0.1.30"
version = "0.1.33"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",