Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 259e1ef0c1 ✏️(frontend) fix feedback-s
Feedback is a mass noun.
Spotted by @bvhme
2025-01-18 22:08:50 +01:00
25 changed files with 1531 additions and 2472 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"eslint", "react", "react-dom", "@types/react-dom", "@types/react", "react-i18next"
"eslint"
]
}
]
+6 -6
View File
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.36.2",
"boto3==1.35.97",
"Brotli==1.1.0",
"brevo-python==1.1.2",
"celery[redis]==5.4.0",
@@ -48,11 +48,11 @@ dependencies = [
"june-analytics-python==2.3.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.4",
"psycopg[binary]==3.2.3",
"PyJWT==2.10.1",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.20.0",
"sentry-sdk==2.19.2",
"url-normalize==1.4.3",
"WeasyPrint>=60.2",
"whitenoise==6.8.2",
@@ -74,7 +74,7 @@ dev = [
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==8.31.0",
"pyfakefs==5.7.4",
"pyfakefs==5.7.3",
"pylint-django==2.6.1",
"pylint==3.3.3",
"pytest-cov==6.0.0",
@@ -82,8 +82,8 @@ dev = [
"pytest==8.3.4",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"responses==0.25.6",
"ruff==0.9.2",
"responses==0.25.5",
"ruff==0.9.1",
"types-requests==2.32.0.20241016",
]
+1417 -1779
View File
File diff suppressed because it is too large Load Diff
+26 -26
View File
@@ -13,23 +13,23 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.8.0",
"@livekit/components-react": "2.6.9",
"@livekit/components-styles": "1.1.4",
"@livekit/track-processors": "0.3.3",
"@pandacss/preset-panda": "0.51.1",
"@react-aria/toast": "3.0.0-beta.19",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.64.2",
"@livekit/track-processors": "0.3.2",
"@pandacss/preset-panda": "0.48.0",
"@react-aria/toast": "3.0.0-beta.18",
"@remixicon/react": "4.5.0",
"@tanstack/react-query": "5.61.3",
"crisp-sdk-web": "1.0.25",
"hoofd": "1.7.3",
"i18next": "24.2.1",
"i18next-browser-languagedetector": "8.0.2",
"i18next-parser": "9.1.0",
"hoofd": "1.7.1",
"i18next": "24.0.2",
"i18next-browser-languagedetector": "8.0.0",
"i18next-parser": "9.0.2",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.8.1",
"posthog-js": "1.207.0",
"livekit-client": "2.6.3",
"posthog-js": "1.188.0",
"react": "18.3.1",
"react-aria-components": "1.6.0",
"react-aria-components": "1.5.0",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"use-sound": "4.0.3",
@@ -37,24 +37,24 @@
"wouter": "3.3.5"
},
"devDependencies": {
"@pandacss/dev": "0.51.1",
"@tanstack/eslint-plugin-query": "5.64.2",
"@tanstack/react-query-devtools": "5.64.2",
"@types/node": "22.10.7",
"@pandacss/dev": "0.48.0",
"@tanstack/eslint-plugin-query": "5.61.3",
"@tanstack/react-query-devtools": "5.61.3",
"@types/node": "22.9.3",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@typescript-eslint/eslint-plugin": "8.21.0",
"@typescript-eslint/parser": "8.21.0",
"@typescript-eslint/eslint-plugin": "8.15.0",
"@typescript-eslint/parser": "8.15.0",
"@vitejs/plugin-react": "4.3.4",
"eslint": "8.57.0",
"eslint-config-prettier": "10.0.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-react-refresh": "0.4.18",
"postcss": "8.5.1",
"prettier": "3.4.2",
"typescript": "5.7.3",
"vite": "6.0.10",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-react-refresh": "0.4.14",
"postcss": "8.4.49",
"prettier": "3.3.3",
"typescript": "5.7.2",
"vite": "6.0.7",
"vite-tsconfig-paths": "5.1.4"
}
}
@@ -32,10 +32,10 @@ const Columns = ({ children }: { children?: ReactNode }) => {
justifyContent: 'normal',
padding: '0 1rem',
width: 'calc(100% - 2rem)',
_motionReduce: {
'@media(prefers-reduced-motion: reduce)': {
opacity: 1,
},
_motionSafe: {
'@media(prefers-reduced-motion: no-preference)': {
opacity: 0,
animation: '.5s ease-in fade 0s forwards',
},
@@ -1,27 +1,8 @@
import { useTranslation } from 'react-i18next'
import {
ParticipantPlaceholder,
usePersistentUserChoices,
usePreviewTracks,
type LocalUserChoices,
} from '@livekit/components-react'
import { css } from '@/styled-system/css'
import { log } from '@livekit/components-core'
import { defaultUserChoices } from '@livekit/components-core'
import { PreJoin, type LocalUserChoices } from '@livekit/components-react'
import { Screen } from '@/layout/Screen'
import { CenteredContent } from '@/layout/CenteredContent'
import { useUser } from '@/features/auth'
import React from 'react'
import {
facingModeFromLocalTrack,
LocalVideoTrack,
Track,
} from 'livekit-client'
import { H } from '@/primitives/H'
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
import { Field } from '@/primitives/Field'
import { Button } from '@/primitives'
const onError = (e: Error) => console.error('ERROR', e)
export const Join = ({
onSubmit,
@@ -30,349 +11,20 @@ export const Join = ({
}) => {
const { t } = useTranslation('rooms')
const { user } = useUser()
const defaults: Partial<LocalUserChoices> = { username: user?.full_name }
const persistUserChoices = true
const joinLabel = t('join.joinLabel')
const userLabel = t('join.usernameLabel')
const [userChoices, setUserChoices] = React.useState(defaultUserChoices)
// TODO: Remove and pipe `defaults` object directly into `usePersistentUserChoices` once we fully switch from type `LocalUserChoices` to `UserChoices`.
const partialDefaults: Partial<LocalUserChoices> = {
...(defaults.audioDeviceId !== undefined && {
audioDeviceId: defaults.audioDeviceId,
}),
...(defaults.videoDeviceId !== undefined && {
videoDeviceId: defaults.videoDeviceId,
}),
...(defaults.audioEnabled !== undefined && {
audioEnabled: defaults.audioEnabled,
}),
...(defaults.videoEnabled !== undefined && {
videoEnabled: defaults.videoEnabled,
}),
...(defaults.username !== undefined && { username: defaults.username }),
}
const {
userChoices: initialUserChoices,
saveAudioInputDeviceId,
saveAudioInputEnabled,
saveVideoInputDeviceId,
saveVideoInputEnabled,
saveUsername,
} = usePersistentUserChoices({
defaults: partialDefaults,
preventSave: !persistUserChoices,
preventLoad: !persistUserChoices,
})
// Initialize device settings
const [audioEnabled, setAudioEnabled] = React.useState<boolean>(
initialUserChoices.audioEnabled
)
const [videoEnabled, setVideoEnabled] = React.useState<boolean>(
initialUserChoices.videoEnabled
)
const [audioDeviceId, setAudioDeviceId] = React.useState<string>(
initialUserChoices.audioDeviceId
)
const [videoDeviceId, setVideoDeviceId] = React.useState<string>(
initialUserChoices.videoDeviceId
)
const [username, setUsername] = React.useState(initialUserChoices.username)
// Save user choices to persistent storage.
React.useEffect(() => {
saveAudioInputEnabled(audioEnabled)
}, [audioEnabled, saveAudioInputEnabled])
React.useEffect(() => {
saveVideoInputEnabled(videoEnabled)
}, [videoEnabled, saveVideoInputEnabled])
React.useEffect(() => {
saveAudioInputDeviceId(audioDeviceId)
}, [audioDeviceId, saveAudioInputDeviceId])
React.useEffect(() => {
saveVideoInputDeviceId(videoDeviceId)
}, [videoDeviceId, saveVideoInputDeviceId])
React.useEffect(() => {
saveUsername(username)
}, [username, saveUsername])
const tracks = usePreviewTracks(
{
audio: audioEnabled
? { deviceId: initialUserChoices.audioDeviceId }
: false,
video: videoEnabled
? { deviceId: initialUserChoices.videoDeviceId }
: false,
},
onError
)
const videoEl = React.useRef(null)
const videoTrack = React.useMemo(
() =>
tracks?.filter(
(track) => track.kind === Track.Kind.Video
)[0] as LocalVideoTrack,
[tracks]
)
const audioTrack = React.useMemo(
() =>
tracks?.filter(
(track) => track.kind === Track.Kind.Audio
)[0] as LocalVideoTrack,
[tracks]
)
const facingMode = React.useMemo(() => {
if (videoTrack) {
const { facingMode } = facingModeFromLocalTrack(videoTrack)
return facingMode
} else {
return 'undefined'
}
}, [videoTrack])
React.useEffect(() => {
if (videoEl.current && videoTrack) {
videoTrack.unmute()
videoTrack.attach(videoEl.current)
}
return () => {
videoTrack?.detach()
}
}, [videoTrack])
const [isValid, setIsValid] = React.useState<boolean>()
const handleValidation = React.useCallback((values: LocalUserChoices) => {
return values.username !== ''
}, [])
React.useEffect(() => {
const newUserChoices = {
username,
videoEnabled,
videoDeviceId,
audioEnabled,
audioDeviceId,
}
setUserChoices(newUserChoices)
setIsValid(handleValidation(newUserChoices))
}, [
username,
videoEnabled,
handleValidation,
audioEnabled,
audioDeviceId,
videoDeviceId,
])
function handleSubmit() {
if (handleValidation(userChoices)) {
if (typeof onSubmit === 'function') {
onSubmit(userChoices)
}
} else {
log.warn('Validation failed with: ', userChoices)
}
}
return (
<Screen footer={false}>
<div
className={css({
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
flexGrow: 1,
lg: {
alignItems: 'center',
},
})}
>
<div
className={css({
display: 'flex',
height: 'auto',
alignItems: 'center',
justifyContent: 'center',
gap: '2rem',
padding: '0 2rem',
flexDirection: 'column',
minWidth: 0,
width: '100%',
lg: {
flexDirection: 'row',
width: 'auto',
height: '570px',
},
})}
>
<div
className={css({
width: '100%',
lg: {
width: '740px',
},
})}
>
<div
className={css({
borderRadius: '1rem',
overflow: 'hidden',
position: 'relative',
width: '100%',
height: 'auto',
aspectRatio: 16 / 9,
'--tw-shadow':
'0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a',
'--tw-shadow-colored':
'0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color)',
boxShadow:
'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)',
})}
>
{videoTrack && (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
ref={videoEl}
width="1280"
height="720"
data-lk-facing-mode={facingMode}
className={css({
display: 'block',
width: '100%',
height: '100%',
objectFit: 'cover',
transform: 'rotateY(180deg)',
})}
/>
)}
{(!videoTrack || !videoEnabled) && (
<div
id="container"
className={css({
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#000',
display: 'grid',
placeItems: 'center',
})}
>
<ParticipantPlaceholder
className={css({
maxWidth: '100%',
height: '70%',
})}
/>
</div>
)}
<div className="lk-button-group-container"></div>
</div>
<div
className={css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '1.5rem',
gap: '1rem',
})}
>
<SelectToggleDevice
source={Track.Source.Microphone}
initialState={audioEnabled}
track={audioTrack}
initialDeviceId={audioDeviceId}
onChange={(enabled) => setAudioEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
onActiveDeviceChange={(deviceId) =>
setAudioDeviceId(deviceId ?? '')
}
variant="tertiary"
/>
<SelectToggleDevice
source={Track.Source.Camera}
initialState={videoEnabled}
track={videoTrack}
initialDeviceId={videoDeviceId}
onChange={(enabled) => {
setVideoEnabled(enabled)
}}
onDeviceError={(error) => console.error(error)}
onActiveDeviceChange={(deviceId) =>
setVideoDeviceId(deviceId ?? '')
}
variant="tertiary"
/>
</div>
</div>
<div
className={css({
width: '100%',
flexShrink: 0,
padding: '0',
sm: {
width: '448px',
padding: '0 3rem 9rem 3rem',
},
})}
>
<form
className={css({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '1rem',
})}
>
<H lvl={1} className={css({ marginBottom: 0 })}>
{t('join.heading')}
</H>
<Field
type="text"
label={userLabel}
defaultValue={username}
onChange={(value) => setUsername(value)}
validate={(value) => {
return !value ? <p>{t('join.errors.usernameEmpty')}</p> : null
}}
className={css({
width: '100%',
})}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
labelProps={{
center: true,
}}
maxLength={50}
/>
<Button
type="submit"
variant={'primary'}
onPress={handleSubmit}
isDisabled={!isValid}
fullWidth
>
{joinLabel}
</Button>
</form>
</div>
</div>
</div>
<Screen layout="centered" footer={false}>
<CenteredContent title={t('join.heading')}>
<PreJoin
persistUserChoices
onSubmit={onSubmit}
micLabel={t('join.audioinput.label')}
camLabel={t('join.videoinput.label')}
joinLabel={t('join.joinLabel')}
userLabel={t('join.usernameLabel')}
defaults={{ username: user?.full_name }}
/>
</CenteredContent>
</Screen>
)
}
@@ -1,123 +0,0 @@
import { Button } from '@/primitives'
import { useMediaDeviceSelect } from '@livekit/components-react'
import { RiCameraSwitchLine } from '@remixicon/react'
import { useEffect, useState } from 'react'
import { ButtonProps } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
enum FacingMode {
USER = 'user',
ENVIRONMENT = 'environment',
}
export const CameraSwitchButton = (props: Partial<ButtonProps>) => {
const { t } = useTranslation('rooms')
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({
kind: 'videoinput',
requestPermissions: true,
})
// getCapabilities type is not available.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getDeviceFacingMode = (device: any): string[] => {
if (!device.getCapabilities) {
return []
}
const capabilities = device.getCapabilities()
if (!capabilities) {
return []
}
if (typeof capabilities.facingMode !== 'object') {
return []
}
return capabilities.facingMode
}
const detectCurrentFacingMode = (): FacingMode | null => {
if (!activeDeviceId) {
return null
}
const activeDevice = devices.find(
(device) => device.deviceId === activeDeviceId
)
if (!activeDevice) {
return null
}
const facingMode = getDeviceFacingMode(activeDevice)
if (facingMode.indexOf(FacingMode.USER) >= 0) {
return FacingMode.USER
}
if (facingMode.indexOf(FacingMode.ENVIRONMENT) >= 0) {
return FacingMode.ENVIRONMENT
}
return null
}
const guessCurrentFacingMode = () => {
const facingMode = detectCurrentFacingMode()
if (facingMode) {
return facingMode
}
// We consider by default if we have no clue that the user camera is used.
return FacingMode.USER
}
const [facingMode, setFacingMode] = useState<FacingMode | null>()
/**
* Before setting the initial value of facingMode we need to wait for devices to
* be loaded ( because in detectCurrentFacingMode we need to find the active device
* in the devices list ), which is not the case at first render.
*/
useEffect(() => {
if (devices.length == 0) {
return
}
if (facingMode) {
return
}
setFacingMode(guessCurrentFacingMode())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [devices])
const getUserDevice = (
facingMode: FacingMode
): MediaDeviceInfo | undefined => {
return devices.find((device) => {
return getDeviceFacingMode(device).indexOf(facingMode) >= 0
})
}
const toggle = () => {
let target: FacingMode
if (facingMode === FacingMode.USER) {
target = FacingMode.ENVIRONMENT
} else {
target = FacingMode.USER
}
const device = getUserDevice(target)
if (device) {
setActiveMediaDevice(device.deviceId)
setFacingMode(target)
} else {
console.error('Cannot get user device with facingMode ' + target)
}
}
return (
<Button
onPress={(e) => {
toggle()
props.onPress?.(e)
}}
variant="primaryTextDark"
aria-label={t('options.items.switchCamera')}
tooltip={t('options.items.switchCamera')}
description={true}
>
<RiCameraSwitchLine size={20} />
</Button>
)
}
@@ -8,7 +8,7 @@ export const OptionsButton = () => {
return (
<>
<Menu variant="dark">
<Menu>
<Button
square
variant="primaryDark"
@@ -26,7 +26,7 @@ export const OptionsMenuItems = () => {
<MenuSection>
<MenuItem
onAction={() => toggleEffects()}
className={menuRecipe({ icon: true, variant: 'dark' }).item}
className={menuRecipe({ icon: true }).item}
>
<RiAccountBoxLine size={20} />
{t('effects')}
@@ -37,13 +37,13 @@ export const OptionsMenuItems = () => {
<MenuItem
href={GRIST_FORM}
target="_blank"
className={menuRecipe({ icon: true, variant: 'dark' }).item}
className={menuRecipe({ icon: true }).item}
>
<RiMegaphoneLine size={20} />
{t('feedback')}
</MenuItem>
<MenuItem
className={menuRecipe({ icon: true, variant: 'dark' }).item}
className={menuRecipe({ icon: true }).item}
onAction={() => setDialogOpen(true)}
>
<RiSettings3Line size={20} />
@@ -46,9 +46,9 @@ export const ScreenShareToggle = ({
{...props}
>
<Div position="relative">
<RiRectangleLine size={24} />
<RiRectangleLine size={28} />
<Icon
size={14}
size={16}
style={{
position: 'absolute',
top: '50%',
@@ -13,14 +13,12 @@ import {
RiVideoOffLine,
RiVideoOnLine,
} from '@remixicon/react'
import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client'
import { Track } from 'livekit-client'
import { Shortcut } from '@/features/shortcuts/types'
import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
import { css } from '@/styled-system/css'
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { useEffect } from 'react'
export type ToggleSource = Exclude<
Track.Source,
@@ -67,22 +65,12 @@ const selectToggleDeviceConfig: SelectToggleDeviceConfigMap = {
type SelectToggleDeviceProps<T extends ToggleSource> =
UseTrackToggleProps<T> & {
track?: LocalAudioTrack | LocalVideoTrack | undefined
initialDeviceId?: string
onActiveDeviceChange: (deviceId: string) => void
source: SelectToggleSource
variant?: NonNullable<ButtonRecipeProps>['variant']
menuVariant?: 'dark' | 'light'
hideMenu?: boolean
}
export const SelectToggleDevice = <T extends ToggleSource>({
track,
initialDeviceId,
onActiveDeviceChange,
hideMenu,
variant = 'primaryDark',
menuVariant = 'light',
...props
}: SelectToggleDeviceProps<T>) => {
const config = selectToggleDeviceConfig[props.source]
@@ -93,19 +81,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
const trackProps = useTrackToggle(props)
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({ kind: config.kind, track })
/**
* When providing only track outside of a room context, activeDeviceId is undefined.
* So we need to initialize it with the initialDeviceId.
* nb: I don't understand why useMediaDeviceSelect cannot infer it from track device id.
*/
useEffect(() => {
if (initialDeviceId !== undefined) {
setActiveMediaDevice(initialDeviceId)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setActiveMediaDevice])
useMediaDeviceSelect({ kind: config.kind })
const selectLabel = t('choose', { keyPrefix: `join.${config.kind}` })
@@ -116,43 +92,29 @@ export const SelectToggleDevice = <T extends ToggleSource>({
gap: '1px',
})}
>
<ToggleDevice
{...trackProps}
config={config}
variant={variant}
toggleButtonProps={{
...(hideMenu
? {
groupPosition: undefined,
}
: {}),
}}
/>
{!hideMenu && (
<Menu variant={menuVariant}>
<Button
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
variant={trackProps.enabled ? variant : 'error2'}
>
<RiArrowDownSLine />
</Button>
<MenuList
items={devices.map((d) => ({
value: d.deviceId,
label: d.label,
}))}
selectedItem={activeDeviceId}
onAction={(value) => {
setActiveMediaDevice(value as string)
onActiveDeviceChange(value as string)
}}
variant={menuVariant}
/>
</Menu>
)}
<ToggleDevice {...trackProps} config={config} />
<Menu>
<Button
tooltip={selectLabel}
aria-label={selectLabel}
groupPosition="right"
square
variant={trackProps.enabled ? 'primaryDark' : 'error2'}
>
<RiArrowDownSLine />
</Button>
<MenuList
items={devices.map((d) => ({
value: d.deviceId,
label: d.label,
}))}
selectedItem={activeDeviceId}
onAction={(value) => {
setActiveMediaDevice(value as string)
onActiveDeviceChange(value as string)
}}
/>
</Menu>
</div>
)
}
@@ -6,28 +6,18 @@ import { useTranslation } from 'react-i18next'
import { SelectToggleDeviceConfig } from './SelectToggleDevice'
import useLongPress from '@/features/shortcuts/useLongPress'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import {
useIsSpeaking,
useLocalParticipant,
useMaybeRoomContext,
} from '@livekit/components-react'
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { useIsSpeaking, useLocalParticipant } from '@livekit/components-react'
export type ToggleDeviceProps = {
enabled: boolean
toggle: () => void
config: SelectToggleDeviceConfig
variant?: NonNullable<ButtonRecipeProps>['variant']
toggleButtonProps?: Partial<ToggleButtonProps>
}
export const ToggleDevice = ({
config,
enabled,
toggle,
variant = 'primaryDark',
toggleButtonProps,
}: ToggleDeviceProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
@@ -58,29 +48,24 @@ export const ToggleDevice = ({
const Icon = enabled ? iconOn : iconOff
const context = useMaybeRoomContext()
if (kind === 'audioinput' && pushToTalk && context) {
return <ActiveSpeakerWrapper />
const { localParticipant } = useLocalParticipant()
const isSpeaking = useIsSpeaking(localParticipant)
if (kind === 'audioinput' && pushToTalk) {
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
}
return (
<ToggleButton
isSelected={!enabled}
variant={enabled ? variant : 'error2'}
variant={enabled ? 'primaryDark' : 'error2'}
shySelected
onPress={() => toggle()}
aria-label={toggleLabel}
tooltip={toggleLabel}
groupPosition="left"
{...toggleButtonProps}
>
<Icon />
</ToggleButton>
)
}
const ActiveSpeakerWrapper = () => {
const { localParticipant } = useLocalParticipant()
const isSpeaking = useIsSpeaking(localParticipant)
return <ActiveSpeaker isSpeaking={isSpeaking} pushToTalk />
}
@@ -62,7 +62,6 @@ export function DesktopControlBar({
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
menuVariant="dark"
/>
<SelectToggleDevice
source={Track.Source.Camera}
@@ -73,7 +72,6 @@ export function DesktopControlBar({
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
menuVariant="dark"
/>
{browserSupportsScreenSharing && (
<ScreenShareToggle
@@ -23,7 +23,6 @@ import { LinkButton } from '@/primitives'
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
import { ResponsiveMenu } from './ResponsiveMenu'
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
import { CameraSwitchButton } from '../../components/controls/CameraSwitchButton'
export function MobileControlBar({
onDeviceError,
@@ -56,7 +55,7 @@ export function MobileControlBar({
className={css({
display: 'flex',
justifyContent: 'space-between',
width: '330px',
width: '422px',
})}
>
<LeaveButton />
@@ -69,7 +68,6 @@ export function MobileControlBar({
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
}
hideMenu={true}
/>
<SelectToggleDevice
source={Track.Source.Camera}
@@ -80,7 +78,6 @@ export function MobileControlBar({
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
}
hideMenu={true}
/>
<HandToggle />
<Button
@@ -177,7 +174,6 @@ export function MobileControlBar({
>
<RiSettings3Line size={20} />
</Button>
<CameraSwitchButton onPress={() => setIsMenuOpened(false)} />
</div>
</div>
</ResponsiveMenu>
+2 -6
View File
@@ -24,10 +24,7 @@
"toggleOff": "",
"toggleOn": "",
"usernameHint": "",
"usernameLabel": "",
"errors": {
"usernameEmpty": ""
}
"usernameLabel": ""
},
"leaveRoomPrompt": "",
"shareDialog": {
@@ -86,8 +83,7 @@
"feedback": "",
"settings": "",
"username": "",
"effects": "",
"switchCamera": ""
"effects": ""
}
},
"effects": {
+3 -7
View File
@@ -18,16 +18,13 @@
"enable": "Enable microphone",
"label": "Microphone"
},
"heading": "Join the meeting",
"heading": "Verify your settings",
"joinLabel": "Join",
"joinMeeting": "Join meeting",
"toggleOff": "Click to turn off",
"toggleOn": "Click to turn on",
"usernameHint": "Shown to other participants",
"usernameLabel": "Your name",
"errors": {
"usernameEmpty": "Your name cannot be empty"
}
"usernameLabel": "Your name"
},
"leaveRoomPrompt": "This will make you leave the meeting.",
"shareDialog": {
@@ -85,8 +82,7 @@
"feedback": "Give us feedback",
"settings": "Settings",
"username": "Update Your Name",
"effects": "Apply effects",
"switchCamera": "Switch camera"
"effects": "Apply effects"
}
},
"effects": {
+3 -7
View File
@@ -18,16 +18,13 @@
"enable": "Activer le micro",
"label": "Microphone"
},
"heading": "Rejoindre la réunion",
"heading": "Vérifiez vos paramètres",
"joinLabel": "Rejoindre",
"joinMeeting": "Rejoindre la réjoindre",
"toggleOff": "Cliquez pour désactiver",
"toggleOn": "Cliquez pour activer",
"usernameHint": "Affiché aux autres participants",
"usernameLabel": "Votre nom",
"errors": {
"usernameEmpty": "Votre nom ne peut pas être vide"
}
"usernameLabel": "Votre nom"
},
"leaveRoomPrompt": "Revenir à l'accueil vous fera quitter la réunion.",
"shareDialog": {
@@ -85,8 +82,7 @@
"feedback": "Partager votre avis",
"settings": "Paramètres",
"username": "Choisir votre nom",
"effects": "Appliquer des effets",
"switchCamera": "Changer de caméra"
"effects": "Appliquer des effets"
}
},
"effects": {
+2 -6
View File
@@ -30,7 +30,7 @@ const box = cva({
},
},
variant: {
light: {
default: {
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'box.border',
@@ -46,10 +46,6 @@ const box = cva({
backgroundColor: 'box.bg',
color: 'control.text',
},
dark: {
backgroundColor: 'primaryDark.50',
borderColord: 'primaryDark.50',
},
},
size: {
default: {
@@ -61,7 +57,7 @@ const box = cva({
},
},
defaultVariants: {
variant: 'light',
variant: 'default',
size: 'default',
},
})
+6 -28
View File
@@ -25,31 +25,11 @@ const FieldWrapper = styled('div', {
marginBottom: 'textfield',
minWidth: 0,
},
variants: {
noMargin: {
true: {
marginBottom: 0,
},
},
fullWidth: {
true: {
width: '100%',
},
},
},
})
const StyledLabel = styled(Label, {
base: {
display: 'block',
fontSize: '0.75rem',
},
variants: {
center: {
true: {
textAlign: 'center',
},
},
},
})
@@ -100,8 +80,6 @@ type FieldProps<T extends object> = (
) & {
label: string
description?: string
wrapperProps?: React.ComponentProps<typeof FieldWrapper>
labelProps?: React.ComponentProps<typeof StyledLabel>
}
/**
@@ -126,7 +104,7 @@ export const Field = <T extends object>({
}: FieldProps<T>) => {
const LabelAndDescription = (
<>
<StyledLabel {...props.labelProps}>{label}</StyledLabel>
<StyledLabel>{label}</StyledLabel>
<FieldDescription slot="description">{description}</FieldDescription>
</>
)
@@ -140,7 +118,7 @@ export const Field = <T extends object>({
if (type === 'text') {
return (
<FieldWrapper {...props.wrapperProps}>
<FieldWrapper>
<RACTextField
validate={validate as unknown as TextFieldProps['validate']}
{...(props as PartialTextFieldProps)}
@@ -155,7 +133,7 @@ export const Field = <T extends object>({
if (type === 'checkbox') {
return (
<FieldWrapper {...props.wrapperProps}>
<FieldWrapper>
<Checkbox
validate={validate as unknown as CheckboxProps['validate']}
description={description}
@@ -169,7 +147,7 @@ export const Field = <T extends object>({
if (type === 'checkboxGroup') {
return (
<FieldWrapper {...props.wrapperProps}>
<FieldWrapper>
<CheckboxGroup
validate={validate as unknown as CheckboxGroupProps['validate']}
{...(props as PartialCheckboxGroupProps)}
@@ -192,7 +170,7 @@ export const Field = <T extends object>({
if (type === 'radioGroup') {
return (
<FieldWrapper {...props.wrapperProps}>
<FieldWrapper>
<RadioGroup
validate={validate as unknown as RadioGroupProps['validate']}
{...(props as PartialRadioGroupProps)}
@@ -211,7 +189,7 @@ export const Field = <T extends object>({
if (type === 'select') {
return (
<FieldWrapper {...props.wrapperProps}>
<FieldWrapper>
<Select
validate={validate as unknown as SelectProps<T>['validate']}
{...(props as PartialSelectProps<T>)}
+1 -3
View File
@@ -8,17 +8,15 @@ import { Box } from './Box'
*/
export const Menu = ({
children,
variant = 'light',
}: {
children: [trigger: ReactNode, menu: ReactNode]
variant?: 'dark' | 'light'
}) => {
const [trigger, menu] = children
return (
<MenuTrigger>
{trigger}
<StyledPopover>
<Box size="sm" type="popover" variant={variant}>
<Box size="sm" type="popover">
{menu}
</Box>
</StyledPopover>
+1 -6
View File
@@ -10,7 +10,6 @@ export const MenuList = <T extends string | number = string>({
onAction,
selectedItem,
items = [],
variant = 'light',
...menuProps
}: {
onAction: (key: T) => void
@@ -19,11 +18,7 @@ export const MenuList = <T extends string | number = string>({
} & MenuProps<unknown> &
RecipeVariantProps<typeof menuRecipe>) => {
const [variantProps] = menuRecipe.splitVariantProps(menuProps)
const classes = menuRecipe({
extraPadding: true,
variant: variant,
...variantProps,
})
const classes = menuRecipe({ extraPadding: true, ...variantProps })
return (
<Menu
selectionMode={selectedItem !== undefined ? 'single' : undefined}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Separator as RACSeparator } from 'react-aria-components'
export const Separator = styled(RACSeparator, {
base: {
height: '1px',
background: 'primaryDark.200',
background: 'gray.400',
margin: '4px 0',
},
})
+2 -6
View File
@@ -46,11 +46,7 @@ export const menuRecipe = sva({
},
},
},
dark: {
item: {
color: 'white',
},
},
dark: {},
},
extraPadding: {
true: {
@@ -71,6 +67,6 @@ export const menuRecipe = sva({
},
},
defaultVariants: {
variant: 'light',
variant: 'dark',
},
})
+1 -1
View File
@@ -14,7 +14,7 @@
"build": "npm run build-mjml-to-html && npm run build-html-to-plain-text"
},
"volta": {
"node": "22.13.0"
"node": "22.11.0"
},
"repository": "https://github.com/numerique-gouv/meet",
"author": "DINUM",
+4 -4
View File
@@ -9,15 +9,15 @@ dependencies = [
"pydantic-settings>=2.1.0",
"celery==5.4.0",
"redis==5.2.1",
"minio==7.2.15",
"openai==1.59.8",
"minio==7.2.14",
"openai==1.59.7",
"requests==2.32.3",
"sentry-sdk[fastapi, celery]==2.20.0",
"sentry-sdk[fastapi, celery]==2.19.2",
]
[project.optional-dependencies]
dev = [
"ruff==0.9.2",
"ruff==0.9.1",
]
[build-system]