💩(frontend) enhance participant's placeholder responsiveness
Duplicated source from LiveKit internal hooks (not ideal, but necessary) to ensure the inner content of the participant placeholder consistently fits its parent container. Unfortunately, I couldn't find a simpler solution. This might lead to some performances issues, as for each tile, some js would be computing avatar's size, which is much more costly than pure css… Note: Gmeet achieves this by generating a temporary placeholder image with a colored background and initials, allowing for a perfectly round, responsive avatar without relying on JavaScript.
This commit is contained in:
committed by
aleb_the_flash
parent
3d91af23cc
commit
86641bd160
@@ -3,6 +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'
|
||||||
|
|
||||||
const StyledParticipantPlaceHolder = styled('div', {
|
const StyledParticipantPlaceHolder = styled('div', {
|
||||||
base: {
|
base: {
|
||||||
@@ -24,17 +26,29 @@ export const ParticipantPlaceholder = ({
|
|||||||
}: ParticipantPlaceholderProps) => {
|
}: ParticipantPlaceholderProps) => {
|
||||||
const isSpeaking = useIsSpeaking(participant)
|
const isSpeaking = useIsSpeaking(participant)
|
||||||
const participantColor = getParticipantColor(participant)
|
const participantColor = getParticipantColor(participant)
|
||||||
|
|
||||||
|
const placeholderEl = useRef<HTMLDivElement>(null)
|
||||||
|
const { width, height } = useSize(placeholderEl)
|
||||||
|
|
||||||
|
const minDimension = Math.min(width, height)
|
||||||
|
const avatarSize = useMemo(
|
||||||
|
() => Math.min(Math.round(minDimension * 0.9), 160),
|
||||||
|
[minDimension]
|
||||||
|
)
|
||||||
|
|
||||||
|
const initialSize = useMemo(() => Math.round(avatarSize * 0.3), [avatarSize])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledParticipantPlaceHolder>
|
<StyledParticipantPlaceHolder ref={placeholderEl}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
animation: isSpeaking ? 'pulse 1s infinite' : undefined,
|
animation: isSpeaking ? 'pulse 1s infinite' : undefined,
|
||||||
width: '80%',
|
|
||||||
maxWidth: '150px',
|
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
aspectRatio: '1/1',
|
aspectRatio: '1/1',
|
||||||
fontSize: '50px',
|
width: '80%',
|
||||||
|
maxWidth: `${avatarSize}px`,
|
||||||
|
fontSize: `${initialSize}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user