Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b452b4c437 | |||
| 1e7aeaf8ce | |||
| 501cfc8d0f | |||
| 0308f98ef9 |
Generated
+52
@@ -13,6 +13,7 @@
|
|||||||
"@livekit/track-processors": "0.5.7",
|
"@livekit/track-processors": "0.5.7",
|
||||||
"@pandacss/preset-panda": "0.54.0",
|
"@pandacss/preset-panda": "0.54.0",
|
||||||
"@react-aria/toast": "3.0.5",
|
"@react-aria/toast": "3.0.5",
|
||||||
|
"@react-hook/size": "2.1.2",
|
||||||
"@remixicon/react": "4.6.0",
|
"@remixicon/react": "4.6.0",
|
||||||
"@tanstack/react-query": "5.81.5",
|
"@tanstack/react-query": "5.81.5",
|
||||||
"@timephy/rnnoise-wasm": "1.0.0",
|
"@timephy/rnnoise-wasm": "1.0.0",
|
||||||
@@ -1210,6 +1211,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@livekit/components-core": {
|
||||||
"version": "0.12.8",
|
"version": "0.12.8",
|
||||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.8.tgz",
|
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.8.tgz",
|
||||||
@@ -2701,6 +2708,51 @@
|
|||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"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": {
|
"node_modules/@react-stately/autocomplete": {
|
||||||
"version": "3.0.0-beta.2",
|
"version": "3.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/autocomplete/-/autocomplete-3.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-stately/autocomplete/-/autocomplete-3.0.0-beta.2.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@livekit/track-processors": "0.5.7",
|
"@livekit/track-processors": "0.5.7",
|
||||||
"@pandacss/preset-panda": "0.54.0",
|
"@pandacss/preset-panda": "0.54.0",
|
||||||
"@react-aria/toast": "3.0.5",
|
"@react-aria/toast": "3.0.5",
|
||||||
|
"@react-hook/size": "2.1.2",
|
||||||
"@remixicon/react": "4.6.0",
|
"@remixicon/react": "4.6.0",
|
||||||
"@tanstack/react-query": "5.81.5",
|
"@tanstack/react-query": "5.81.5",
|
||||||
"@timephy/rnnoise-wasm": "1.0.0",
|
"@timephy/rnnoise-wasm": "1.0.0",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
MediaDeviceFailure,
|
MediaDeviceFailure,
|
||||||
Room,
|
Room,
|
||||||
RoomOptions,
|
RoomOptions,
|
||||||
|
supportsAdaptiveStream,
|
||||||
|
supportsDynacast,
|
||||||
} from 'livekit-client'
|
} from 'livekit-client'
|
||||||
import { keys } from '@/api/queryKeys'
|
import { keys } from '@/api/queryKeys'
|
||||||
import { queryClient } from '@/api/queryClient'
|
import { queryClient } from '@/api/queryClient'
|
||||||
@@ -74,8 +76,8 @@ export const Conference = ({
|
|||||||
|
|
||||||
const roomOptions = useMemo((): RoomOptions => {
|
const roomOptions = useMemo((): RoomOptions => {
|
||||||
return {
|
return {
|
||||||
adaptiveStream: true,
|
adaptiveStream: supportsAdaptiveStream(),
|
||||||
dynacast: true,
|
dynacast: supportsDynacast(),
|
||||||
publishDefaults: {
|
publishDefaults: {
|
||||||
videoCodec: 'vp9',
|
videoCodec: 'vp9',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useMemo, useRef } from 'react'
|
|||||||
import { ScreenSharePreferenceStore } from '@/stores/ScreenSharePreferences'
|
import { ScreenSharePreferenceStore } from '@/stores/ScreenSharePreferences'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { useLocalParticipant } from '@livekit/components-react'
|
import { useLocalParticipant } from '@livekit/components-react'
|
||||||
import { useSize } from '../hooks/useResizeObserver'
|
|
||||||
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSize from '@react-hook/size'
|
||||||
|
|
||||||
export const FullScreenShareWarning = ({
|
export const FullScreenShareWarning = ({
|
||||||
trackReference,
|
trackReference,
|
||||||
@@ -16,7 +16,7 @@ export const FullScreenShareWarning = ({
|
|||||||
const { t } = useTranslation('rooms', { keyPrefix: 'fullScreenWarning' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'fullScreenWarning' })
|
||||||
|
|
||||||
const warningContainerRef = useRef<HTMLDivElement>(null)
|
const warningContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const { width: containerWidth } = useSize(warningContainerRef)
|
const containerWidth = useSize(warningContainerRef)[0]
|
||||||
const { localParticipant } = useLocalParticipant()
|
const { localParticipant } = useLocalParticipant()
|
||||||
const screenSharePreferences = useSnapshot(ScreenSharePreferenceStore)
|
const screenSharePreferences = useSnapshot(ScreenSharePreferenceStore)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { styled } from '@/styled-system/jsx'
|
|||||||
import { Avatar } from '@/components/Avatar'
|
import { Avatar } from '@/components/Avatar'
|
||||||
import { useIsSpeaking } from '@livekit/components-react'
|
import { useIsSpeaking } from '@livekit/components-react'
|
||||||
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
|
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
|
||||||
import { useSize } from '@/features/rooms/livekit/hooks/useResizeObserver'
|
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
|
import useSize from '@react-hook/size'
|
||||||
|
|
||||||
const StyledParticipantPlaceHolder = styled('div', {
|
const StyledParticipantPlaceHolder = styled('div', {
|
||||||
base: {
|
base: {
|
||||||
@@ -28,7 +28,7 @@ export const ParticipantPlaceholder = ({
|
|||||||
const participantColor = getParticipantColor(participant)
|
const participantColor = getParticipantColor(participant)
|
||||||
|
|
||||||
const placeholderEl = useRef<HTMLDivElement>(null)
|
const placeholderEl = useRef<HTMLDivElement>(null)
|
||||||
const { width, height } = useSize(placeholderEl)
|
const [width, height] = useSize(placeholderEl)
|
||||||
|
|
||||||
const minDimension = Math.min(width, height)
|
const minDimension = Math.min(width, height)
|
||||||
const avatarSize = useMemo(
|
const avatarSize = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
/* 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 { ToolsToggle } from '../../components/controls/ToolsToggle'
|
||||||
import { InfoToggle } from '../../components/controls/InfoToggle'
|
import { InfoToggle } from '../../components/controls/InfoToggle'
|
||||||
import { AdminToggle } from '../../components/AdminToggle'
|
import { AdminToggle } from '../../components/AdminToggle'
|
||||||
import { useSize } from '../../hooks/useResizeObserver'
|
|
||||||
import { useState, RefObject } from 'react'
|
import { useState, RefObject } from 'react'
|
||||||
import { Dialog, DialogTrigger, Popover } from 'react-aria-components'
|
import { Dialog, DialogTrigger, Popover } from 'react-aria-components'
|
||||||
import { Button } from '@/primitives'
|
import { Button } from '@/primitives'
|
||||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||||
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
import { RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import useSize from '@react-hook/size'
|
||||||
|
|
||||||
const CONTROL_BAR_BREAKPOINT = 1100
|
const CONTROL_BAR_BREAKPOINT = 1100
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export const MoreOptions = ({
|
|||||||
}: {
|
}: {
|
||||||
parentElement: RefObject<HTMLDivElement>
|
parentElement: RefObject<HTMLDivElement>
|
||||||
}) => {
|
}) => {
|
||||||
const { width: parentWidth } = useSize(parentElement)
|
const parentWidth = useSize(parentElement)[0]
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { RoomEvent, Track } from 'livekit-client'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
CarouselLayout,
|
|
||||||
ConnectionStateToast,
|
ConnectionStateToast,
|
||||||
FocusLayoutContainer,
|
FocusLayoutContainer,
|
||||||
GridLayout,
|
GridLayout,
|
||||||
@@ -32,6 +31,7 @@ import { RecordingStateToast } from '@/features/recording'
|
|||||||
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
|
||||||
import { useConnectionObserver } from '../hooks/useConnectionObserver'
|
import { useConnectionObserver } from '../hooks/useConnectionObserver'
|
||||||
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
||||||
|
import { CarouselLayout } from '../components/layout/CarouselLayout'
|
||||||
|
|
||||||
const LayoutWrapper = styled(
|
const LayoutWrapper = styled(
|
||||||
'div',
|
'div',
|
||||||
|
|||||||
Reference in New Issue
Block a user