Compare commits

..

10 Commits

Author SHA1 Message Date
lebaudantoine cec81ba101 📱(frontend) improve feedback screen responsiveness on mobile
Tighten the feedback screen layout on mobile viewports so it fits
comfortably on small phone screens and reads naturally without
horizontal scroll or cramped controls.
2026-08-24 14:43:58 +02:00
lebaudantoine a1ecf994c7 📱(frontend) stack idle modal buttons in a column on mobile
Wrap the buttons in the idle modal so they stack vertically on
mobile viewports.

The horizontal layout was cramped on small screens; a column layout
gives each button a comfortable touch target and reads more
naturally on narrow displays.
2026-08-24 14:43:58 +02:00
lebaudantoine 04fc9fa1ab 📱(frontend) collapse mobile control bar items on narrow viewports
Apply the same approach as the PiP screen to the mobile control
bar: make sure it never overflows on small phone screens by
collapsing some controls into the overflow menu when there is not
enough horizontal room.
2026-08-24 14:43:58 +02:00
lebaudantoine f18d784615 🔖(minor) bump release to 1.28.0 2026-08-24 11:32:06 +02:00
lebaudantoine 1bfe6b8977 🔇(frontend) suppress leaked WebSocket error events from livekit-client
livekit-client leaks the raw WebSocket error Event as an unhandled
rejection when the signal WebSocket errors after connect. This is
mostly seen on Firefox and coincides with signal reconnects.

The event carries zero diagnostic content — the close reason is
already logged by the SDK — so it only adds noise to error
tracking.

Filter it out from our reporting.

Upstream issue: https://github.com/livekit/client-sdk-js/issues/2062
2026-08-23 18:12:18 +02:00
lebaudantoine 84845709d0 🐛(frontend) make the device-in-use logic race-free
The device-in-use logic could race when multiple detection events
fired close together (e.g. rapid mic/cam toggles or several devices
becoming busy at once), leading to inconsistent store state.

Refactor the flow so all updates go through a serialized path,
removing the race conditions and keeping the in-use state
consistent regardless of event ordering.
2026-08-23 17:40:01 +02:00
lebaudantoine e000377b5c ♻️(frontend) track device-in-use state by id instead of by kind
Refactor the device-in-use handling to key state by device id
instead of only by kind (microphone or camera).

On computers with several devices of the same kind, keying by kind
meant that one device being in use marked the whole kind as busy,
even when the user could still use another device. Tracking per id
lets the UI and logic distinguish between devices correctly.

The store is updated accordingly so that per-device state is stored
and read consistently across the app.
2026-08-23 17:40:01 +02:00
lebaudantoine a9fa3eb4ba 🐛(frontend) treat "Timeout starting source" AbortError as device-in-use
Also consider `AbortError` with a message like "Timeout starting
video/audio source" as an "already in use" device error.

This can happen for other reasons in theory, but in practice most
occurrences are caused by another application still holding the
camera or microphone.

Route it through the existing device-in-use handling so users get
the same clear explanation as with the standard error, instead of a
generic failure.
2026-08-23 17:40:01 +02:00
lebaudantoine 2ec3f54532 🐛(frontend) handle Firefox/Windows AbortError on device start
Some versions of Firefox on Windows do not raise the
`NotReadableError` that LiveKit expects when the camera or
microphone fails to start. Instead, they surface an `AbortError`
with a message explaining the browser could not start the video or
microphone input.

This case was observed in PostHog error tracking and was not
handled, so users hit an unhelpful failure state.

Detect that specific `AbortError` and route it through the same
device-in-use / not-readable handling as the standard error, so
users get a clear explanation.
2026-08-23 17:40:01 +02:00
lebaudantoine d723c14d84 🐛(frontend) handle device-in-use errors on Chrome / Windows 10
On Chrome and Windows 10, when the camera or microphone is already
in use by another application, the browser rejects
`getUserMedia`. Without dedicated handling, users just saw their
camera or microphone not turning on, without any explanation.

Detect this specific failure and surface a clear message to the
user, so they understand another application is holding the device
and know what to do about it.
2026-08-23 17:40:01 +02:00
20 changed files with 249 additions and 100 deletions
+9
View File
@@ -8,6 +8,14 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Changed
- 📱(frontend) collapse mobile control bar items on narrow viewports
- 📱(frontend) stack idle modal buttons in a column on mobile
- 📱(frontend) improve feedback screen responsiveness on mobile
## [1.28.0] - 2026-08-24
### Added ### Added
- 📈(frontend) track errors when starting or stopping a recording - 📈(frontend) track errors when starting or stopping a recording
@@ -35,6 +43,7 @@ and this project adheres to
- 🐛(frontend) handle device-in-use errors on Chrome / Windows 10 - 🐛(frontend) handle device-in-use errors on Chrome / Windows 10
- 🐛(frontend) handle Firefox/Windows AbortError on device start - 🐛(frontend) handle Firefox/Windows AbortError on device start
- 🐛(frontend) treat "Timeout starting source" AbortError as device-in-use - 🐛(frontend) treat "Timeout starting source" AbortError as device-in-use
- 🔇(frontend) suppress leaked WebSocket error events from livekit-client
## [1.27.0] - 2026-08-14 ## [1.27.0] - 2026-08-14
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "agents" name = "agents"
version = "1.27.0" version = "1.28.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"livekit-agents==1.6.7", "livekit-agents==1.6.7",
+1 -1
View File
@@ -9,7 +9,7 @@ resolution-markers = [
[[package]] [[package]]
name = "agents" name = "agents"
version = "1.27.0" version = "1.28.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "livekit-agents" }, { name = "livekit-agents" },
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project] [project]
name = "meet" name = "meet"
version = "1.27.0" version = "1.28.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }] authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -1187,7 +1187,7 @@ wheels = [
[[package]] [[package]]
name = "meet" name = "meet"
version = "1.27.0" version = "1.28.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "meet", "name": "meet",
"version": "1.27.0", "version": "1.28.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "meet", "name": "meet",
"version": "1.27.0", "version": "1.28.0",
"dependencies": { "dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6", "@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11", "@fontsource-variable/lexend": "5.2.11",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "meet", "name": "meet",
"private": true, "private": true,
"version": "1.27.0", "version": "1.28.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "panda codegen && vite", "dev": "panda codegen && vite",
@@ -2,6 +2,12 @@ import type { CaptureResult } from 'posthog-js'
const IGNORED_EXCEPTION_PATTERNS = [ const IGNORED_EXCEPTION_PATTERNS = [
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/, /ResizeObserver loop (completed with undelivered notifications|limit exceeded)/,
// livekit-client leaks the raw WebSocket error Event as an unhandled
// rejection when the signal ws errors after connect (Firefox-heavy,
// coincides with signal reconnects). Carries zero diagnostic content —
// the close reason is already logged by the SDK.
// See: https://github.com/livekit/client-sdk-js/issues/2062
/^Event captured as exception with keys: isTrusted$/,
] ]
const shouldIgnoreException = (value: unknown): boolean => const shouldIgnoreException = (value: unknown): boolean =>
@@ -13,7 +13,7 @@ const controlBarRegion = cva({
mobile: { mobile: {
true: { true: {
justifyContent: 'center', justifyContent: 'center',
width: '330px', width: '100%',
}, },
}, },
}, },
@@ -1,4 +1,3 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiEmotionLine } from '@remixicon/react' import { RiEmotionLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives' import { ToggleButton } from '@/primitives'
@@ -7,6 +6,8 @@ import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKey
import { REACTIONS_TOOLBAR_ID } from '../constants' import { REACTIONS_TOOLBAR_ID } from '../constants'
import { useReactionsToolbar } from '../hooks/useReactionsToolbar' import { useReactionsToolbar } from '../hooks/useReactionsToolbar'
import { layoutStore } from '@/stores/layout' import { layoutStore } from '@/stores/layout'
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
const focusReactionsToolbar = () => { const focusReactionsToolbar = () => {
document document
@@ -17,35 +18,44 @@ const focusReactionsToolbar = () => {
export const REACTIONS_TOGGLE_ID = 'reactions-toggle' export const REACTIONS_TOGGLE_ID = 'reactions-toggle'
export const ReactionsToggle = () => { /* eslint-disable react-refresh/only-export-components */
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' }) export const reactionShortcutHandler = () => {
const { isOpen, toggle } = useReactionsToolbar()
const handleShortcut = useCallback(() => {
if (layoutStore.showReactionsToolbar) { if (layoutStore.showReactionsToolbar) {
focusReactionsToolbar() focusReactionsToolbar()
} else { } else {
layoutStore.showReactionsToolbar = true layoutStore.showReactionsToolbar = true
} }
}, []) }
type Props = Pick<NonNullable<ButtonRecipeProps>, 'variant'> & ToggleButtonProps
export const ReactionsToggle = ({
variant = 'primaryDark',
onPress,
...props
}: Props) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { isOpen, toggle } = useReactionsToolbar()
useRegisterKeyboardShortcut({ useRegisterKeyboardShortcut({
id: 'reaction', id: 'reaction',
handler: handleShortcut, handler: reactionShortcutHandler,
}) })
return ( return (
<ToggleButton <ToggleButton
{...props}
id={REACTIONS_TOGGLE_ID} id={REACTIONS_TOGGLE_ID}
data-attr="reactions-toggle" data-attr="reactions-toggle"
square square
variant="primaryDark" variant={variant}
aria-label={t('button')} aria-label={t('button')}
aria-expanded={isOpen} aria-expanded={isOpen}
tooltip={t('button')} tooltip={t('button')}
isSelected={isOpen} isSelected={isOpen}
onChange={toggle} onChange={toggle}
onPress={onPress}
> >
<RiEmotionLine /> <RiEmotionLine />
</ToggleButton> </ToggleButton>
@@ -15,7 +15,8 @@ const Card = styled('div', {
marginTop: '1.5rem', marginTop: '1.5rem',
borderRadius: '0.25rem', borderRadius: '0.25rem',
boxShadow: '', boxShadow: '',
minWidth: '380px', width: '100%',
maxWidth: '380px',
minHeight: '196px', minHeight: '196px',
}, },
}) })
@@ -37,8 +38,11 @@ const ratingButtonRecipe = cva({
color: 'initial', color: 'initial',
border: 'none', border: 'none',
borderRadius: 0, borderRadius: 0,
padding: '0.5rem 0.85rem', padding: { base: '0.5rem 0.25rem', xsm: '0.5rem 0.85rem' },
flexGrow: '1', flexGrow: '1',
flexBasis: 0,
minWidth: 0,
textAlign: 'center',
cursor: 'pointer', cursor: 'pointer',
}, },
variants: { variants: {
@@ -99,7 +103,9 @@ const OpenFeedback = ({
return ( return (
<Card> <Card>
<H lvl={3}>{t('question')}</H> <H lvl={3} centered>
{t('question')}
</H>
<TextArea <TextArea
id="feedbackInput" id="feedbackInput"
name="feedback" name="feedback"
@@ -168,7 +174,9 @@ const RateQuality = ({
return ( return (
<Card> <Card>
<H lvl={3}>{t('question')}</H> <H lvl={3} centered>
{t('question')}
</H>
<Bar> <Bar>
{[...Array(maxRating)].map((_, index) => ( {[...Array(maxRating)].map((_, index) => (
<RACButton <RACButton
@@ -228,7 +236,9 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
}} }}
> >
<VStack gap={0}> <VStack gap={0}>
<H lvl={3}>{t('heading')}</H> <H lvl={3} centered>
{t('heading')}
</H>
<Text as="p" variant="paragraph" centered> <Text as="p" variant="paragraph" centered>
{t('body')} {t('body')}
</Text> </Text>
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { connectionObserverStore } from '@/stores/connectionObserver' import { connectionObserverStore } from '@/stores/connectionObserver'
import { HStack } from '@/styled-system/jsx' import { Stack } from '@/styled-system/jsx'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { navigateTo } from '@/navigation/navigateTo' import { navigateTo } from '@/navigation/navigateTo'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce' import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
@@ -134,7 +134,11 @@ export const IsIdleDisconnectModal = () => {
</H> </H>
<Description /> <Description />
<Settings /> <Settings />
<HStack marginTop="2rem"> <Stack
direction={{ base: 'column', xsm: 'row' }}
align={{ base: 'stretch', xsm: 'center' }}
marginTop="2rem"
>
<Button <Button
onPress={() => { onPress={() => {
connectionObserverStore.isIdleDisconnectModalOpen = false connectionObserverStore.isIdleDisconnectModalOpen = false
@@ -148,7 +152,7 @@ export const IsIdleDisconnectModal = () => {
<Button onPress={close} size="sm" variant="primary"> <Button onPress={close} size="sm" variant="primary">
{t('stayButton')} {t('stayButton')}
</Button> </Button>
</HStack> </Stack>
</div> </div>
) )
}} }}
@@ -10,10 +10,18 @@ import {
showLowerHandToast, showLowerHandToast,
} from '@/features/notifications/utils' } from '@/features/notifications/utils'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut' import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
const SPEAKING_DETECTION_DELAY = 3000 const SPEAKING_DETECTION_DELAY = 3000
export const HandToggle = () => { type Props = Pick<NonNullable<ButtonRecipeProps>, 'variant'> & ToggleButtonProps
export const HandToggle = ({
variant = 'primaryDark',
onPress,
...props
}: Props) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' }) const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
const room = useRoomContext() const room = useRoomContext()
@@ -74,12 +82,16 @@ export const HandToggle = () => {
})} })}
> >
<ToggleButton <ToggleButton
{...props}
square square
variant="primaryDark" variant={variant}
aria-label={t(tooltipLabel)} aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)} tooltip={t(tooltipLabel)}
isSelected={isHandRaised} isSelected={isHandRaised}
onPress={handleToggle} onPress={(e) => {
handleToggle()
onPress?.(e)
}}
data-attr={`controls-hand-${tooltipLabel}`} data-attr={`controls-hand-${tooltipLabel}`}
> >
<RiHand /> <RiHand />
@@ -1,7 +1,7 @@
import { supportsScreenSharing } from '@livekit/components-core' import { supportsScreenSharing } from '@livekit/components-core'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { ControlBarAuxProps } from './ControlBar' import type { ControlBarAuxProps } from './ControlBar'
import React from 'react' import React, { useLayoutEffect, useRef, useState } from 'react'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { LeaveButton } from '../../components/controls/LeaveButton' import { LeaveButton } from '../../components/controls/LeaveButton'
import { Track } from 'livekit-client' import { Track } from 'livekit-client'
@@ -26,7 +26,26 @@ import { AudioDevicesControl } from '../../components/controls/Device/AudioDevic
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl' import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
import { openSettingsDialog } from '@/stores/settings' import { openSettingsDialog } from '@/stores/settings'
import { ControlBarRegion } from '@/features/layout/components/ControlBarRegion' import { ControlBarRegion } from '@/features/layout/components/ControlBarRegion'
import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle' import {
ReactionsToggle,
reactionShortcutHandler,
} from '@/features/reactions/components/ReactionsToggle'
import { useSize } from '../../hooks/useResizeObserver'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { useRoomContext } from '@livekit/components-react'
// Hand collapses first, then reactions; hidden toggles move into the menu.
const COLLAPSIBLE_COUNT = 2
// Layout-neutral measuring wrapper: inherits the region's gap and refuses to
// flex-shrink so measured widths are natural content widths.
const measuredRow = css({
display: 'inline-flex',
alignItems: 'center',
gap: 'inherit',
flexShrink: 0,
})
export function MobileControlBar({ export function MobileControlBar({
onDeviceError, onDeviceError,
@@ -36,19 +55,72 @@ export function MobileControlBar({
const browserSupportsScreenSharing = supportsScreenSharing() const browserSupportsScreenSharing = supportsScreenSharing()
const { toggleEffects } = useSidePanel() const { toggleEffects } = useSidePanel()
const containerRef = useRef<HTMLDivElement>(null)
const { width } = useSize(containerRef)
const barRef = useRef<HTMLDivElement>(null)
const { width: barWidth } = useSize(barRef)
const collapsibleRef = useRef<HTMLDivElement>(null)
const { width: collapsibleWidth } = useSize(collapsibleRef)
const [hiddenCount, setHiddenCount] = useState(0)
const calibration = useRef<{ essential: number; slot: number }>()
useLayoutEffect(() => {
if (hiddenCount === 0 && collapsibleWidth > 0 && barRef.current) {
const gap = parseFloat(getComputedStyle(barRef.current).columnGap) || 0
calibration.current = {
essential: barWidth - collapsibleWidth - gap,
slot: (collapsibleWidth + gap) / COLLAPSIBLE_COUNT,
}
}
if (!calibration.current || width <= 0) return
const { essential, slot } = calibration.current
const fits = Math.floor((width - essential) / slot)
const next = Math.min(
COLLAPSIBLE_COUNT,
Math.max(0, COLLAPSIBLE_COUNT - fits)
)
if (next !== hiddenCount) setHiddenCount(next)
}, [barWidth, collapsibleWidth, hiddenCount, width, setHiddenCount])
const hideHand = hiddenCount >= 1
const hideReactions = hiddenCount >= 2
const room = useRoomContext()
const { toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
useRegisterKeyboardShortcut({
id: 'raise-hand',
handler: toggleRaisedHand,
})
useRegisterKeyboardShortcut({
id: 'reaction',
handler: reactionShortcutHandler,
})
const { data } = useConfig() const { data } = useConfig()
const closeMenu = () => setIsMenuOpened(false)
return ( return (
<> <>
<div <div
className={css({ className={css({
width: '100vw', width: '100vw',
display: 'flex',
padding: '1.125rem', padding: '1.125rem',
})}
>
<div
ref={containerRef}
className={css({
width: '100%',
display: 'flex',
justifyContent: 'center', justifyContent: 'center',
})} })}
> >
<ControlBarRegion mobile> <ControlBarRegion mobile>
<div ref={barRef} className={measuredRow}>
<LeaveButton /> <LeaveButton />
<AudioDevicesControl <AudioDevicesControl
onDeviceError={(error) => onDeviceError={(error) =>
@@ -62,8 +134,13 @@ export function MobileControlBar({
} }
hideMenu={true} hideMenu={true}
/> />
{/* Unmounted when empty so it doesn't leave a stray gap. */}
{!hideReactions && (
<div ref={collapsibleRef} className={measuredRow}>
<ReactionsToggle /> <ReactionsToggle />
<HandToggle /> {!hideHand && <HandToggle />}
</div>
)}
<Button <Button
id="room-options-trigger" id="room-options-trigger"
square square
@@ -74,12 +151,11 @@ export function MobileControlBar({
> >
<RiMore2Line /> <RiMore2Line />
</Button> </Button>
</div>
</ControlBarRegion> </ControlBarRegion>
</div> </div>
<ResponsiveMenu </div>
isOpened={isMenuOpened} <ResponsiveMenu isOpened={isMenuOpened} onClosed={closeMenu}>
onClosed={() => setIsMenuOpened(false)}
>
<div <div
className={css({ className={css({
display: 'flex', display: 'flex',
@@ -98,6 +174,20 @@ export function MobileControlBar({
}, },
})} })}
> >
{hideReactions && (
<ReactionsToggle
variant="primaryTextDark"
description={true}
onPress={closeMenu}
/>
)}
{hideHand && (
<HandToggle
variant="primaryTextDark"
description={true}
onPress={closeMenu}
/>
)}
{browserSupportsScreenSharing && ( {browserSupportsScreenSharing && (
<ScreenShareToggle <ScreenShareToggle
onDeviceError={(error) => onDeviceError={(error) =>
@@ -105,25 +195,16 @@ export function MobileControlBar({
} }
variant="primaryTextDark" variant="primaryTextDark"
description={true} description={true}
onPress={() => setIsMenuOpened(false)} onPress={closeMenu}
/> />
)} )}
<ChatToggle <ChatToggle description={true} onPress={closeMenu} />
description={true} <ParticipantsToggle description={true} onPress={closeMenu} />
onPress={() => setIsMenuOpened(false)} <ToolsToggle description={true} onPress={closeMenu} />
/>
<ParticipantsToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<ToolsToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<Button <Button
onPress={() => { onPress={() => {
toggleEffects() toggleEffects()
setIsMenuOpened(false) closeMenu()
}} }}
variant="primaryTextDark" variant="primaryTextDark"
aria-label={t('options.items.effects')} aria-label={t('options.items.effects')}
@@ -140,7 +221,7 @@ export function MobileControlBar({
aria-label={t('options.items.feedback')} aria-label={t('options.items.feedback')}
description={true} description={true}
target="_blank" target="_blank"
onPress={() => setIsMenuOpened(false)} onPress={closeMenu}
> >
<RiMegaphoneLine size={20} /> <RiMegaphoneLine size={20} />
</LinkButton> </LinkButton>
@@ -148,7 +229,7 @@ export function MobileControlBar({
<Button <Button
onPress={() => { onPress={() => {
openSettingsDialog() openSettingsDialog()
setIsMenuOpened(false) closeMenu()
}} }}
variant="primaryTextDark" variant="primaryTextDark"
aria-label={t('options.items.settings')} aria-label={t('options.items.settings')}
@@ -157,7 +238,7 @@ export function MobileControlBar({
> >
<RiSettings3Line size={20} /> <RiSettings3Line size={20} />
</Button> </Button>
<CameraSwitchButton onPress={() => setIsMenuOpened(false)} /> <CameraSwitchButton onPress={closeMenu} />
</div> </div>
</div> </div>
</ResponsiveMenu> </ResponsiveMenu>
@@ -1,7 +1,8 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/primitives' import { Button } from '@/primitives'
import { Screen } from '@/layout/Screen' import { Screen } from '@/layout/Screen'
import { Center, HStack, styled, VStack } from '@/styled-system/jsx' import { Center, Stack, styled, VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { Rating } from '@/features/rooms/components/Rating.tsx' import { Rating } from '@/features/rooms/components/Rating.tsx'
import { useLocation } from 'wouter' import { useLocation } from 'wouter'
import { useMemo } from 'react' import { useMemo } from 'react'
@@ -14,14 +15,18 @@ const Heading = styled('h1', {
fontStyle: 'normal', fontStyle: 'normal',
fontStretch: 'normal', fontStretch: 'normal',
fontOpticalSizing: 'auto', fontOpticalSizing: 'auto',
fontSize: '2.3rem', fontSize: { base: '1.75rem', xsm: '2.3rem' },
lineHeight: '2.5rem', lineHeight: { base: '2.125rem', xsm: '2.5rem' },
letterSpacing: '0', letterSpacing: '0',
paddingBottom: '2rem', paddingBottom: { base: '1.5rem', xsm: '2rem' },
textAlign: 'center', textAlign: 'center',
}, },
}) })
const buttonClass = css({
width: { base: '100%', xsm: 'auto' },
})
enum DisconnectReasonKey { enum DisconnectReasonKey {
DuplicateIdentity = 'duplicateIdentity', DuplicateIdentity = 'duplicateIdentity',
ParticipantRemoved = 'participantRemoved', ParticipantRemoved = 'participantRemoved',
@@ -54,19 +59,31 @@ const FeedbackRoute = () => {
return ( return (
<Screen layout="centered" footer={false}> <Screen layout="centered" footer={false}>
<Center> <Center width="100%">
<VStack> <VStack width="100%" paddingX="1rem">
<Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading> <Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading>
<HStack> <Stack
direction={{ base: 'column', xsm: 'row' }}
width={{ base: '100%', xsm: 'auto' }}
maxWidth="380px"
>
{showBackButton && ( {showBackButton && (
<Button variant="secondary" onPress={() => window.history.back()}> <Button
variant="secondary"
className={buttonClass}
onPress={() => window.history.back()}
>
{t('feedback.back')} {t('feedback.back')}
</Button> </Button>
)} )}
<Button variant="primary" onPress={() => setLocation('/')}> <Button
variant="primary"
className={buttonClass}
onPress={() => setLocation('/')}
>
{t('feedback.home')} {t('feedback.home')}
</Button> </Button>
</HStack> </Stack>
<Rating metadata={metadata} /> <Rating metadata={metadata} />
</VStack> </VStack>
</Center> </Center>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.27.0", "version": "1.28.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.27.0", "version": "1.28.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@html-to/text-cli": "0.6.1", "@html-to/text-cli": "0.6.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "mail_mjml", "name": "mail_mjml",
"version": "1.27.0", "version": "1.28.0",
"description": "An util to generate html and text django's templates from mjml templates", "description": "An util to generate html and text django's templates from mjml templates",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "sdk", "name": "sdk",
"version": "1.27.0", "version": "1.28.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sdk", "name": "sdk",
"version": "1.27.0", "version": "1.28.0",
"license": "ISC", "license": "ISC",
"workspaces": [ "workspaces": [
"./library", "./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "sdk", "name": "sdk",
"version": "1.27.0", "version": "1.28.0",
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"description": "", "description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "summary" name = "summary"
version = "1.27.0" version = "1.28.0"
dependencies = [ dependencies = [
"fastapi[standard]>=0.105.0", "fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0", "uvicorn>=0.24.0",