Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 035f8fb44f 🐛(frontend) restore automatic lower-hand on speaking
Following the re-rendering optimization refactoring, the automatic
lower-hand feature broke: the way `isSpeaking` was read no longer
made sense once we limited how often components in the app
re-render.

Fix the detection so the raised hand is again lowered automatically
when the participant starts speaking, without relying on frequent
re-renders.
2026-09-03 23:24:28 +02:00
8 changed files with 66 additions and 21 deletions
-3
View File
@@ -17,15 +17,12 @@ and this project adheres to
### Changed
- ⬆️(dev) pin LiveKit server to v1.13.6
- 🔒(frontend) upgrade base image to 1.30.4-alpine3.24
### Fixed
- 🐛(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
- 🐛(frontend) keep feedback buttons on one line for fr/es/en
## [1.30.0] - 2026-09-01
+11 -3
View File
@@ -54,11 +54,19 @@ RUN npx webpack --mode production
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.30.3-alpine3.23 AS frontend-production
USER root
RUN apk del curl
USER nginx
# Security patches for known CVEs
RUN apk update && apk upgrade \
libcrypto3>=3.5.7-r0 \
libssl3>=3.5.7-r0 \
musl \
musl-utils \
zlib>=1.3.2-r0 \
libexpat>=2.8.4-r0 \
&& apk del curl
USER nginx
-1
View File
@@ -1,6 +1,5 @@
:root {
--fonts-sans: 'Marianne', ui-sans-serif, system-ui, sans-serif;
--avatar-cap-height: 0.7;
}
.Header-beforeLogo {
-1
View File
@@ -34,7 +34,6 @@ 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;
}
```
+12 -2
View File
@@ -42,10 +42,20 @@ ENV VITE_APP_TITLE=${VITE_APP_TITLE}
RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.30.3-alpine3.23 AS frontend-production
USER root
RUN apk del curl
# Security patches for known CVEs
RUN apk update && apk upgrade \
libcrypto3>=3.5.7-r0 \
libssl3>=3.5.7-r0 \
musl \
musl-utils \
zlib>=1.3.2-r0 \
libexpat>=2.8.4-r0 \
&& apk del curl
USER nginx
# Un-privileged user running the application
+42 -6
View File
@@ -1,5 +1,5 @@
import { css, cva, RecipeVariantProps } from '@/styled-system/css'
import React, { useMemo } from 'react'
import React, { useLayoutEffect, useMemo } from 'react'
const avatar = cva({
base: {
@@ -28,17 +28,24 @@ 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] ?? ''
}
@@ -59,6 +66,36 @@ 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 }}
@@ -71,16 +108,15 @@ export const Avatar = React.memo(
className={css({ width: '100%', height: '100%', display: 'block' })}
>
<text
ref={textRef}
x="50"
y={50}
y="50"
transform={`translate(0 ${offsetY})`}
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>
@@ -65,7 +65,7 @@ const FeedbackRoute = () => {
<Stack
direction={{ base: 'column', xsm: 'row' }}
width={{ base: '100%', xsm: 'auto' }}
maxWidth="410px"
maxWidth="380px"
>
{showBackButton && (
<Button
-4
View File
@@ -6,10 +6,6 @@ body,
height: 100%;
}
:root {
--avatar-cap-height: 0.7;
}
html.font-lexend {
--fonts-sans: 'Lexend Variable', ui-sans-serif, system-ui, sans-serif;
}