mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-05 15:16:38 +00:00
Compare commits
2 Commits
fix/perf-issues
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| adc74f846c | |||
| 78ba03a52b |
@@ -22,6 +22,8 @@ and this project adheres to
|
||||
|
||||
- 🐛(backend) allow any printable ASCII characters in user sub field #1673
|
||||
- 🐛(frontend) keep the sending resolution picked while the camera is off #1667
|
||||
- 🐛(frontend) restore automatic lower-hand on speaking
|
||||
- 🐛(frontend) center Avatar initials with a font-aware cap-height ratio
|
||||
|
||||
## [1.30.0] - 2026-09-01
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
:root {
|
||||
--fonts-sans: 'Marianne', ui-sans-serif, system-ui, sans-serif;
|
||||
--avatar-cap-height: 0.7;
|
||||
}
|
||||
|
||||
.Header-beforeLogo {
|
||||
|
||||
@@ -34,6 +34,7 @@ Let's say you want to change the font of our application to a custom font. You c
|
||||
|
||||
:root {
|
||||
--fonts-sans: 'Roboto', ui-sans-serif, system-ui, sans-serif;
|
||||
--avatar-cap-height: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css, cva, RecipeVariantProps } from '@/styled-system/css'
|
||||
import React, { useLayoutEffect, useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
const avatar = cva({
|
||||
base: {
|
||||
@@ -28,24 +28,17 @@ const avatar = cva({
|
||||
},
|
||||
})
|
||||
|
||||
// Instantiating a segmenter is expensive; create it once and reuse it.
|
||||
const graphemeSegmenter =
|
||||
typeof Intl !== 'undefined' && 'Segmenter' in Intl
|
||||
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
||||
: undefined
|
||||
|
||||
/**
|
||||
* Returns the first user-perceived character. Some Unicode characters span
|
||||
* multiple UTF-16 code units, so a naive index into the string can split them
|
||||
* and yield a broken glyph.
|
||||
*/
|
||||
const getFirstGrapheme = (value: string): string => {
|
||||
if (!value) return ''
|
||||
if (graphemeSegmenter) {
|
||||
const [first] = graphemeSegmenter.segment(value)
|
||||
return first?.segment ?? ''
|
||||
}
|
||||
// Fallback: keeps single code points intact (including surrogate pairs).
|
||||
return Array.from(value)[0] ?? ''
|
||||
}
|
||||
|
||||
@@ -66,36 +59,6 @@ export type AvatarProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
export const Avatar = React.memo(
|
||||
({ name, bgColor, context, notification, style, ...props }: AvatarProps) => {
|
||||
const initials = useMemo(() => getInitials(name), [name])
|
||||
const textRef = React.useRef<SVGTextElement>(null)
|
||||
const [offsetY, setOffsetY] = React.useState(0)
|
||||
|
||||
// Optically center the initials: measure the ink bounding box of the
|
||||
// rendered glyphs and shift them so the box's center sits at the middle
|
||||
// of the viewBox. Works for any font, weight or glyph shape, unlike a
|
||||
// hand-tuned dy offset. getBBox() is in local (pre-transform)
|
||||
// coordinates, so applying the translation never changes the measure.
|
||||
useLayoutEffect(() => {
|
||||
const text = textRef.current
|
||||
if (!text) return
|
||||
|
||||
const center = () => {
|
||||
const box = text.getBBox()
|
||||
// A hidden element measures as an empty box; keep the default then.
|
||||
if (box.height === 0) return
|
||||
setOffsetY(50 - (box.y + box.height / 2))
|
||||
}
|
||||
|
||||
center()
|
||||
// Glyph metrics can change once webfonts finish loading.
|
||||
let cancelled = false
|
||||
document.fonts?.ready.then(() => {
|
||||
if (!cancelled) center()
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initials])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ backgroundColor: bgColor, ...style }}
|
||||
@@ -108,15 +71,16 @@ export const Avatar = React.memo(
|
||||
className={css({ width: '100%', height: '100%', display: 'block' })}
|
||||
>
|
||||
<text
|
||||
ref={textRef}
|
||||
x="50"
|
||||
y="50"
|
||||
transform={`translate(0 ${offsetY})`}
|
||||
y={50}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fontSize="52"
|
||||
fontWeight="500"
|
||||
fill="currentColor"
|
||||
className={css({
|
||||
transform:
|
||||
'translateY(calc(var(--avatar-cap-height, 0.7) * 0.5em))',
|
||||
})}
|
||||
>
|
||||
{initials}
|
||||
</text>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { RiHand } from '@remixicon/react'
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { useIsSpeaking, useRoomContext } from '@livekit/components-react'
|
||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
@@ -25,11 +25,11 @@ export const HandToggle = ({
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
||||
|
||||
const room = useRoomContext()
|
||||
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||
const { isHandRaised, toggleRaisedHand, lowerHand } = useRaisedHand({
|
||||
participant: room.localParticipant,
|
||||
})
|
||||
|
||||
const isSpeaking = room.localParticipant.isSpeaking
|
||||
const isSpeaking = useIsSpeaking(room.localParticipant)
|
||||
const speakingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [hasShownToast, setHasShownToast] = useState(false)
|
||||
|
||||
@@ -57,9 +57,10 @@ export const HandToggle = ({
|
||||
|
||||
if (shouldShowToast && !speakingTimerRef.current) {
|
||||
speakingTimerRef.current = setTimeout(() => {
|
||||
speakingTimerRef.current = null
|
||||
setHasShownToast(true)
|
||||
const onClose = () => {
|
||||
if (isHandRaised) toggleRaisedHand()
|
||||
lowerHand()
|
||||
resetToastState()
|
||||
}
|
||||
showLowerHandToast(room.localParticipant, onClose)
|
||||
@@ -70,7 +71,17 @@ export const HandToggle = ({
|
||||
speakingTimerRef.current = null
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSpeaking, isHandRaised, hasShownToast, toggleRaisedHand])
|
||||
}, [isSpeaking, isHandRaised, hasShownToast, lowerHand])
|
||||
|
||||
// Clear any pending timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (speakingTimerRef.current) {
|
||||
clearTimeout(speakingTimerRef.current)
|
||||
speakingTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
||||
|
||||
|
||||
@@ -86,5 +86,16 @@ export function useRaisedHand({ participant }: useRaisedHandProps) {
|
||||
}
|
||||
}
|
||||
|
||||
return { isHandRaised, toggleRaisedHand }
|
||||
const lowerHand = async () => {
|
||||
if (!isLocal(participant)) return
|
||||
try {
|
||||
await raiseHand(false)
|
||||
} catch (e) {
|
||||
reportError('generic_failure', e, {
|
||||
context: 'lower_raised_hand',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { isHandRaised, toggleRaisedHand, lowerHand }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ body,
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:root {
|
||||
--avatar-cap-height: 0.7;
|
||||
}
|
||||
|
||||
html.font-lexend {
|
||||
--fonts-sans: 'Lexend Variable', ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user