Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 3384f0f3c7 🔖(minor) bump release to 1.28.0 2026-08-24 11:31:19 +02:00
10 changed files with 121 additions and 278 deletions
-11
View File
@@ -8,17 +8,6 @@ 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
- ⬆️(frontend) upgrade @fontsource-variable/atkinson-hyperlegible-next
- ⬆️(frontend) upgrade i18next-resources-to-backend from 1.2.1 to 1.2.3
- ⬆️(frontend) upgrade @tanstack/react-query from 5.101.1 to 5.101.4
- ⬆️(frontend) upgrade @pandacss/preset-panda from 1.11.3 to 1.12.0
- ⬆️(frontend) upgrade posthog-js from 1.404.1 to 1.409.5
## [1.28.0] - 2026-08-24 ## [1.28.0] - 2026-08-24
### Added ### Added
+30 -42
View File
@@ -8,7 +8,7 @@
"name": "meet", "name": "meet",
"version": "1.28.0", "version": "1.28.0",
"dependencies": { "dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0", "@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11", "@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5", "@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4", "@libreaudio/la-call": "0.1.4",
@@ -16,10 +16,10 @@
"@livekit/components-styles": "1.2.0", "@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2", "@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.14", "@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.12.0", "@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0", "@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0", "@remixicon/react": "4.9.0",
"@tanstack/react-query": "5.101.4", "@tanstack/react-query": "5.101.1",
"@timephy/rnnoise-wasm": "1.0.0", "@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.1.2", "crisp-sdk-web": "1.1.2",
"hoofd": "1.7.3", "hoofd": "1.7.3",
@@ -27,9 +27,9 @@
"i18next": "26.3.6", "i18next": "26.3.6",
"i18next-browser-languagedetector": "8.2.1", "i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0", "i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.3", "i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.21.0", "livekit-client": "2.21.0",
"posthog-js": "1.409.5", "posthog-js": "1.404.1",
"react": "18.3.1", "react": "18.3.1",
"react-aria": "3.50.0", "react-aria": "3.50.0",
"react-aria-components": "1.19.0", "react-aria-components": "1.19.0",
@@ -767,9 +767,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@fontsource-variable/atkinson-hyperlegible-next": { "node_modules/@fontsource-variable/atkinson-hyperlegible-next": {
"version": "5.3.0", "version": "5.2.6",
"resolved": "https://registry.npmjs.org/@fontsource-variable/atkinson-hyperlegible-next/-/atkinson-hyperlegible-next-5.3.0.tgz", "resolved": "https://registry.npmjs.org/@fontsource-variable/atkinson-hyperlegible-next/-/atkinson-hyperlegible-next-5.2.6.tgz",
"integrity": "sha512-UnLzNUy1RWoLDfydAatP4NaRm99X/WbQEph7q/PymJnCFIOOwKOx3pBmp5ys8trO0dM+CKGuJgeZP9fAR4mQpA==", "integrity": "sha512-D0Z8peFkRGYACvmWp8Sl8YPGYLBEJbzqapugtsMEYSUUtpQDiZVDHjk9s7bqB4hSCRYCrKuW6V1V+mnHybVnPw==",
"license": "OFL-1.1", "license": "OFL-1.1",
"funding": { "funding": {
"url": "https://github.com/sponsors/ayuhito" "url": "https://github.com/sponsors/ayuhito"
@@ -1642,21 +1642,21 @@
} }
}, },
"node_modules/@pandacss/preset-panda": { "node_modules/@pandacss/preset-panda": {
"version": "1.12.0", "version": "1.11.3",
"resolved": "https://registry.npmjs.org/@pandacss/preset-panda/-/preset-panda-1.12.0.tgz", "resolved": "https://registry.npmjs.org/@pandacss/preset-panda/-/preset-panda-1.11.3.tgz",
"integrity": "sha512-jhzhAmtGY34b1FlMJGEAh/IkfU4WyTNT2mpJfcBUPBi1rZLHSTcs0aqedRmMdwM4tTT43XFOXMiazbztKME60A==", "integrity": "sha512-IHeRQyIYeGKpQl78A/oI9fGg8MHQiOX/HttH+iQNaRU/MbGN6JsPe/N0ekQC6QY8iiKtLhqR+SRc455SN6JVWw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@pandacss/types": "1.12.0" "@pandacss/types": "1.11.3"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@pandacss/preset-panda/node_modules/@pandacss/types": { "node_modules/@pandacss/preset-panda/node_modules/@pandacss/types": {
"version": "1.12.0", "version": "1.11.3",
"resolved": "https://registry.npmjs.org/@pandacss/types/-/types-1.12.0.tgz", "resolved": "https://registry.npmjs.org/@pandacss/types/-/types-1.11.3.tgz",
"integrity": "sha512-PtlEIOBEV3RzWffeu1zH+/YFmP+fn24M63STGEbgTwjEBMhyo/FfT2sfZGB8oWAbWUBKBp69OMg25E+/rY3klQ==", "integrity": "sha512-tUbxa3WzoCuulP6kcMn7f0uNgLBvu81JAg4sfoUpz6PZE36VHA3k9n6ROEwziTbr7vqgeCj5DYQCUldrVNbEGQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20" "node": ">=20"
@@ -1719,16 +1719,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@posthog/browser-common": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.3.1.tgz",
"integrity": "sha512-1nhMVY1wnHADTg8tR9yvm+lPAz5ROxznfQlBtLzB2FFo4lp/LU8lk9KyFPsARDkBxCfYachsJfvRjocL1G/AJQ==",
"license": "MIT",
"dependencies": {
"@posthog/core": "^1.46.0",
"@posthog/types": "^1.399.0"
}
},
"node_modules/@posthog/core": { "node_modules/@posthog/core": {
"version": "1.48.3", "version": "1.48.3",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.48.3.tgz", "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.48.3.tgz",
@@ -2424,9 +2414,9 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.101.4", "version": "5.101.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.1.tgz",
"integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", "integrity": "sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -2445,12 +2435,12 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.101.4", "version": "5.101.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.1.tgz",
"integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", "integrity": "sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "5.101.4" "@tanstack/query-core": "5.101.1"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -6985,10 +6975,9 @@
} }
}, },
"node_modules/i18next-resources-to-backend": { "node_modules/i18next-resources-to-backend": {
"version": "1.2.3", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.3.tgz", "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz",
"integrity": "sha512-8Y/LLAm5fqZc2ckQxtTWbu75ndjNLzF7mcYl6rhc4g+WBopIsG3YhdYqF5qhJy64tlYhlU2KSSnEgW3gGHLqwg==", "integrity": "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
} }
@@ -9134,14 +9123,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.409.5", "version": "1.404.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.409.5.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.404.1.tgz",
"integrity": "sha512-s1iJz+vq0YAluUD4OwLjUFIJt/9pqiiB6MITvHWIS16a7pqTJqbSLXsd5/8kJcIgfrOSZHe+mdYAXLYcJCS4LQ==", "integrity": "sha512-ck/HOMKuZ6yefUk5OX+h2s9mUWBsnu0mGDUAWjIwAmQQrloZPShzOhOWuEuZQuMnCKORBFRVGsBAuzsl66cBJA==",
"license": "(Apache-2.0 AND MIT)", "license": "(Apache-2.0 AND MIT)",
"dependencies": { "dependencies": {
"@posthog/browser-common": "^0.3.1", "@posthog/core": "^1.43.1",
"@posthog/core": "^1.46.1", "@posthog/types": "^1.397.0",
"@posthog/types": "^1.399.0",
"core-js": "^3.49.0", "core-js": "^3.49.0",
"dompurify": "^3.3.2", "dompurify": "^3.3.2",
"fflate": "^0.4.8", "fflate": "^0.4.8",
+5 -5
View File
@@ -15,7 +15,7 @@
"check": "prettier --check ./src" "check": "prettier --check ./src"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0", "@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
"@fontsource-variable/lexend": "5.2.11", "@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5", "@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4", "@libreaudio/la-call": "0.1.4",
@@ -23,10 +23,10 @@
"@livekit/components-styles": "1.2.0", "@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2", "@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.14", "@mediapipe/tasks-vision": "0.10.14",
"@pandacss/preset-panda": "1.12.0", "@pandacss/preset-panda": "1.11.3",
"@react-types/overlays": "3.10.0", "@react-types/overlays": "3.10.0",
"@remixicon/react": "4.9.0", "@remixicon/react": "4.9.0",
"@tanstack/react-query": "5.101.4", "@tanstack/react-query": "5.101.1",
"@timephy/rnnoise-wasm": "1.0.0", "@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.1.2", "crisp-sdk-web": "1.1.2",
"hoofd": "1.7.3", "hoofd": "1.7.3",
@@ -34,9 +34,9 @@
"i18next": "26.3.6", "i18next": "26.3.6",
"i18next-browser-languagedetector": "8.2.1", "i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0", "i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.3", "i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.21.0", "livekit-client": "2.21.0",
"posthog-js": "1.409.5", "posthog-js": "1.404.1",
"react": "18.3.1", "react": "18.3.1",
"react-aria": "3.50.0", "react-aria": "3.50.0",
"react-aria-components": "1.19.0", "react-aria-components": "1.19.0",
@@ -13,7 +13,7 @@ const controlBarRegion = cva({
mobile: { mobile: {
true: { true: {
justifyContent: 'center', justifyContent: 'center',
width: '100%', width: '330px',
}, },
}, },
}, },
@@ -1,3 +1,4 @@
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'
@@ -6,8 +7,6 @@ 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
@@ -18,44 +17,35 @@ const focusReactionsToolbar = () => {
export const REACTIONS_TOGGLE_ID = 'reactions-toggle' export const REACTIONS_TOGGLE_ID = 'reactions-toggle'
/* eslint-disable react-refresh/only-export-components */ export const ReactionsToggle = () => {
export const reactionShortcutHandler = () => {
if (layoutStore.showReactionsToolbar) {
focusReactionsToolbar()
} else {
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 { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { isOpen, toggle } = useReactionsToolbar() const { isOpen, toggle } = useReactionsToolbar()
const handleShortcut = useCallback(() => {
if (layoutStore.showReactionsToolbar) {
focusReactionsToolbar()
} else {
layoutStore.showReactionsToolbar = true
}
}, [])
useRegisterKeyboardShortcut({ useRegisterKeyboardShortcut({
id: 'reaction', id: 'reaction',
handler: reactionShortcutHandler, handler: handleShortcut,
}) })
return ( return (
<ToggleButton <ToggleButton
{...props}
id={REACTIONS_TOGGLE_ID} id={REACTIONS_TOGGLE_ID}
data-attr="reactions-toggle" data-attr="reactions-toggle"
square square
variant={variant} variant="primaryDark"
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,8 +15,7 @@ const Card = styled('div', {
marginTop: '1.5rem', marginTop: '1.5rem',
borderRadius: '0.25rem', borderRadius: '0.25rem',
boxShadow: '', boxShadow: '',
width: '100%', minWidth: '380px',
maxWidth: '380px',
minHeight: '196px', minHeight: '196px',
}, },
}) })
@@ -38,11 +37,8 @@ const ratingButtonRecipe = cva({
color: 'initial', color: 'initial',
border: 'none', border: 'none',
borderRadius: 0, borderRadius: 0,
padding: { base: '0.5rem 0.25rem', xsm: '0.5rem 0.85rem' }, padding: '0.5rem 0.85rem',
flexGrow: '1', flexGrow: '1',
flexBasis: 0,
minWidth: 0,
textAlign: 'center',
cursor: 'pointer', cursor: 'pointer',
}, },
variants: { variants: {
@@ -103,9 +99,7 @@ const OpenFeedback = ({
return ( return (
<Card> <Card>
<H lvl={3} centered> <H lvl={3}>{t('question')}</H>
{t('question')}
</H>
<TextArea <TextArea
id="feedbackInput" id="feedbackInput"
name="feedback" name="feedback"
@@ -174,9 +168,7 @@ const RateQuality = ({
return ( return (
<Card> <Card>
<H lvl={3} centered> <H lvl={3}>{t('question')}</H>
{t('question')}
</H>
<Bar> <Bar>
{[...Array(maxRating)].map((_, index) => ( {[...Array(maxRating)].map((_, index) => (
<RACButton <RACButton
@@ -236,9 +228,7 @@ const ConfirmationMessage = ({ onNext }: { onNext: () => void }) => {
}} }}
> >
<VStack gap={0}> <VStack gap={0}>
<H lvl={3} centered> <H lvl={3}>{t('heading')}</H>
{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 { Stack } from '@/styled-system/jsx' import { HStack } 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,11 +134,7 @@ export const IsIdleDisconnectModal = () => {
</H> </H>
<Description /> <Description />
<Settings /> <Settings />
<Stack <HStack marginTop="2rem">
direction={{ base: 'column', xsm: 'row' }}
align={{ base: 'stretch', xsm: 'center' }}
marginTop="2rem"
>
<Button <Button
onPress={() => { onPress={() => {
connectionObserverStore.isIdleDisconnectModalOpen = false connectionObserverStore.isIdleDisconnectModalOpen = false
@@ -152,7 +148,7 @@ export const IsIdleDisconnectModal = () => {
<Button onPress={close} size="sm" variant="primary"> <Button onPress={close} size="sm" variant="primary">
{t('stayButton')} {t('stayButton')}
</Button> </Button>
</Stack> </HStack>
</div> </div>
) )
}} }}
@@ -10,18 +10,10 @@ 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
type Props = Pick<NonNullable<ButtonRecipeProps>, 'variant'> & ToggleButtonProps export const HandToggle = () => {
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()
@@ -82,16 +74,12 @@ export const HandToggle = ({
})} })}
> >
<ToggleButton <ToggleButton
{...props}
square square
variant={variant} variant="primaryDark"
aria-label={t(tooltipLabel)} aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)} tooltip={t(tooltipLabel)}
isSelected={isHandRaised} isSelected={isHandRaised}
onPress={(e) => { onPress={handleToggle}
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, { useLayoutEffect, useRef, useState } from 'react' import React 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,26 +26,7 @@ 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 { import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
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,
@@ -55,107 +36,50 @@ 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',
justifyContent: 'center',
})} })}
> >
<div <ControlBarRegion mobile>
ref={containerRef} <LeaveButton />
className={css({ <AudioDevicesControl
width: '100%', onDeviceError={(error) =>
display: 'flex', onDeviceError?.({ source: Track.Source.Microphone, error })
justifyContent: 'center', }
})} hideMenu={true}
> />
<ControlBarRegion mobile> <VideoDeviceControl
<div ref={barRef} className={measuredRow}> onDeviceError={(error) =>
<LeaveButton /> onDeviceError?.({ source: Track.Source.Camera, error })
<AudioDevicesControl }
onDeviceError={(error) => hideMenu={true}
onDeviceError?.({ source: Track.Source.Microphone, error }) />
} <ReactionsToggle />
hideMenu={true} <HandToggle />
/> <Button
<VideoDeviceControl id="room-options-trigger"
onDeviceError={(error) => square
onDeviceError?.({ source: Track.Source.Camera, error }) variant="primaryDark"
} aria-label={t('options.buttonLabel')}
hideMenu={true} tooltip={t('options.buttonLabel')}
/> onPress={() => setIsMenuOpened(true)}
{/* Unmounted when empty so it doesn't leave a stray gap. */} >
{!hideReactions && ( <RiMore2Line />
<div ref={collapsibleRef} className={measuredRow}> </Button>
<ReactionsToggle /> </ControlBarRegion>
{!hideHand && <HandToggle />}
</div>
)}
<Button
id="room-options-trigger"
square
variant="primaryDark"
aria-label={t('options.buttonLabel')}
tooltip={t('options.buttonLabel')}
onPress={() => setIsMenuOpened(true)}
>
<RiMore2Line />
</Button>
</div>
</ControlBarRegion>
</div>
</div> </div>
<ResponsiveMenu isOpened={isMenuOpened} onClosed={closeMenu}> <ResponsiveMenu
isOpened={isMenuOpened}
onClosed={() => setIsMenuOpened(false)}
>
<div <div
className={css({ className={css({
display: 'flex', display: 'flex',
@@ -174,20 +98,6 @@ 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) =>
@@ -195,16 +105,25 @@ export function MobileControlBar({
} }
variant="primaryTextDark" variant="primaryTextDark"
description={true} description={true}
onPress={closeMenu} onPress={() => setIsMenuOpened(false)}
/> />
)} )}
<ChatToggle description={true} onPress={closeMenu} /> <ChatToggle
<ParticipantsToggle description={true} onPress={closeMenu} /> description={true}
<ToolsToggle description={true} onPress={closeMenu} /> onPress={() => setIsMenuOpened(false)}
/>
<ParticipantsToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<ToolsToggle
description={true}
onPress={() => setIsMenuOpened(false)}
/>
<Button <Button
onPress={() => { onPress={() => {
toggleEffects() toggleEffects()
closeMenu() setIsMenuOpened(false)
}} }}
variant="primaryTextDark" variant="primaryTextDark"
aria-label={t('options.items.effects')} aria-label={t('options.items.effects')}
@@ -221,7 +140,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={closeMenu} onPress={() => setIsMenuOpened(false)}
> >
<RiMegaphoneLine size={20} /> <RiMegaphoneLine size={20} />
</LinkButton> </LinkButton>
@@ -229,7 +148,7 @@ export function MobileControlBar({
<Button <Button
onPress={() => { onPress={() => {
openSettingsDialog() openSettingsDialog()
closeMenu() setIsMenuOpened(false)
}} }}
variant="primaryTextDark" variant="primaryTextDark"
aria-label={t('options.items.settings')} aria-label={t('options.items.settings')}
@@ -238,7 +157,7 @@ export function MobileControlBar({
> >
<RiSettings3Line size={20} /> <RiSettings3Line size={20} />
</Button> </Button>
<CameraSwitchButton onPress={closeMenu} /> <CameraSwitchButton onPress={() => setIsMenuOpened(false)} />
</div> </div>
</div> </div>
</ResponsiveMenu> </ResponsiveMenu>
@@ -1,8 +1,7 @@
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, Stack, styled, VStack } from '@/styled-system/jsx' import { Center, HStack, 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'
@@ -15,18 +14,14 @@ const Heading = styled('h1', {
fontStyle: 'normal', fontStyle: 'normal',
fontStretch: 'normal', fontStretch: 'normal',
fontOpticalSizing: 'auto', fontOpticalSizing: 'auto',
fontSize: { base: '1.75rem', xsm: '2.3rem' }, fontSize: '2.3rem',
lineHeight: { base: '2.125rem', xsm: '2.5rem' }, lineHeight: '2.5rem',
letterSpacing: '0', letterSpacing: '0',
paddingBottom: { base: '1.5rem', xsm: '2rem' }, paddingBottom: '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',
@@ -59,31 +54,19 @@ const FeedbackRoute = () => {
return ( return (
<Screen layout="centered" footer={false}> <Screen layout="centered" footer={false}>
<Center width="100%"> <Center>
<VStack width="100%" paddingX="1rem"> <VStack>
<Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading> <Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading>
<Stack <HStack>
direction={{ base: 'column', xsm: 'row' }}
width={{ base: '100%', xsm: 'auto' }}
maxWidth="380px"
>
{showBackButton && ( {showBackButton && (
<Button <Button variant="secondary" onPress={() => window.history.back()}>
variant="secondary"
className={buttonClass}
onPress={() => window.history.back()}
>
{t('feedback.back')} {t('feedback.back')}
</Button> </Button>
)} )}
<Button <Button variant="primary" onPress={() => setLocation('/')}>
variant="primary"
className={buttonClass}
onPress={() => setLocation('/')}
>
{t('feedback.home')} {t('feedback.home')}
</Button> </Button>
</Stack> </HStack>
<Rating metadata={metadata} /> <Rating metadata={metadata} />
</VStack> </VStack>
</Center> </Center>