Compare commits

..

3 Commits

Author SHA1 Message Date
lebaudantoine 27af71b9f4 🚸(frontend) explain to a user her was ejected
Add a clear feedback message explaining to users when they are ejected from a
meeting, explicitly stating that the action was taken by an admin.
2026-01-11 11:23:29 +01:00
lebaudantoine e88c80044a ♻️(frontend) refactor disconnection reason handling and state
Refactor the duplicateIdentity boolean URL parameter into an extensible string
reason parameter, making it easier to customize the disconnection message
shown to users.

Avoid passing this value via URL parameters, which are easy to manipulate.
Instead, use Wouter’s built-in navigation state to pass data across pages.

This was initially missed because navigateTo is a wrapper around Wouter’s
official navigation function, and its arguments were easy to overlook. This is
now fixed.

This prepares the ground for supporting additional disconnection reasons in
upcoming commits.
2026-01-10 18:45:44 +01:00
lebaudantoine f886aa90c2 ♻️(frontend) refactor the onDisconnected function to use a switch
This makes the logic more extensible in preparation for introducing
additional disconnect reason handlers.
2026-01-10 18:42:56 +01:00
48 changed files with 164 additions and 554 deletions
-15
View File
@@ -8,20 +8,6 @@ and this project adheres to
## [Unreleased]
### Changed
- ♿️(frontend) add accessible back button in side panel #881
- ♿️(frontend) improve participants toggle a11y label #880
- ♿️(frontend) make carousel image decorative #871
- ♿️(frontend) reactions are now vocalized and configurable #849
### Fixed
- 🔒(backend) prevent automatic upgrade setuptools
- ♿(frontend) improve contrast for selected options #863
- ♿️(frontend) announce copy state in invite dialog #877
## [1.3.0] - 2026-01-13
### Added
@@ -40,7 +26,6 @@ and this project adheres to
### Fixed
- 🐛(frontend) remove unexpected F2 tooltip when clicking video screen
- 🩹(frontend) icon font loading to avoid text/icon flickering
## [1.2.0] - 2026-01-05
+1 -1
View File
@@ -4,7 +4,7 @@
FROM python:3.13.5-alpine3.21 AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip
RUN python -m pip install --upgrade pip setuptools
# Upgrade system packages to install security updates
RUN apk update && \
+1 -1
View File
@@ -1,6 +1,6 @@
# Installation with docker compose
We provide a sample configuration for running Meet using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Meet in production is to use [k8s](../installation/kubernetes.md).
We provide a sample configuration for running Meet using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Meet in production is to use [k8s](../installation/k8s.md)
## Requirements
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "1.3.0"
version = "1.2.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.3.10",
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "1.3.0"
version = "1.2.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
-16
View File
@@ -6,22 +6,6 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<!-- Font URLs are resolved and replaced by Vite during the build process. Font loading failures will not break the application. -->
<link
rel="preload"
as="font"
crossorigin="anonymous"
href="/node_modules/@fontsource/material-icons-outlined/files/material-icons-outlined-latin-400-normal.woff2"
type="font/woff2"
/>
<!-- Font URLs are resolved and replaced by Vite during the build process. Font loading failures will not break the application. -->
<link
rel="preload"
as="font"
crossorigin="anonymous"
href="/node_modules/@fontsource-variable/material-symbols-outlined/files/material-symbols-outlined-latin-wght-normal.woff2"
type="font/woff2"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE%</title>
</head>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "1.3.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "1.3.0",
"version": "1.2.0",
"dependencies": {
"@fontsource-variable/material-symbols-outlined": "5.2.30",
"@fontsource/material-icons-outlined": "5.2.6",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "1.3.0",
"version": "1.2.0",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -192,7 +192,7 @@ export const IntroSlider = () => {
<SlideContainer>
{SLIDES.map((slide, index) => (
<Slide visible={index == slideIndex} key={index}>
<Image src={slide.src} alt="" role="presentation" />
<Image src={slide.src} alt={t(`${slide.key}.imgAlt`)} />
<TextAnimation visible={index == slideIndex}>
<Heading>{t(`${slide.key}.title`)}</Heading>
<Body>{t(`${slide.key}.body`)}</Body>
@@ -1,7 +1,7 @@
import { css } from '@/styled-system/css'
import { css, cx } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { Spinner } from '@/primitives/Spinner'
import { Button, Icon, Text } from '@/primitives'
import { Button, Text } from '@/primitives'
import { useTranslation } from 'react-i18next'
import { RecordingStatuses } from '../hooks/useRecordingStatuses'
import { ReactNode, useEffect, useRef, useState } from 'react'
@@ -141,23 +141,31 @@ export const ControlsButton = ({
})}
onPress={() => openSidePanel()}
>
<Icon
className={css({
color: 'primary.500',
marginRight: '1rem',
})}
name="info"
/>
<span
className={cx(
'material-icons',
css({
color: 'primary.500',
marginRight: '1rem',
})
)}
>
info
</span>
<Text variant={'smNote'}>
{parseLineBreaks(t('button.anotherModeStarted'))}
</Text>
<Icon
className={css({
color: 'primary.500',
marginLeft: 'auto',
})}
name="chevron_right"
/>
<span
className={cx(
'material-icons',
css({
color: 'primary.500',
marginLeft: 'auto',
})
)}
>
chevron_right
</span>
</RACButton>
)}
<Button
@@ -1,4 +1,4 @@
import { H, Text, Icon } from '@/primitives'
import { H, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { LoginButton } from '@/components/LoginButton'
import { HStack } from '@/styled-system/jsx'
@@ -24,7 +24,9 @@ export const LoginPrompt = ({ heading, body }: LoginPromptProps) => {
})}
>
<HStack justify="start" alignItems="center" marginBottom="0.5rem">
<Icon type="symbols" name="login" />
<span className="material-symbols" aria-hidden={true}>
login
</span>
<H lvl={3} margin={false} padding={false}>
{heading}
</H>
@@ -1,5 +1,4 @@
import { Spinner } from '@/primitives/Spinner'
import { Icon } from '@/primitives'
interface RecordingStatusIconProps {
isStarted: boolean
@@ -15,8 +14,8 @@ export const RecordingStatusIcon = ({
}
if (isTranscriptActive) {
return <Icon type="symbols" name="speech_to_text" />
return <span className="material-symbols">speech_to_text</span>
}
return <Icon type="symbols" name="screen_record" />
return <span className="material-symbols">screen_record</span>
}
@@ -1,4 +1,4 @@
import { Button, Icon, H, Text } from '@/primitives'
import { Button, H, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { useEffect, useRef, useState } from 'react'
@@ -59,7 +59,7 @@ export const RequestRecording = ({
})}
>
<HStack justify="start" alignItems="center" marginBottom="0.5rem">
<Icon type="symbols" name="person_raised_hand" />
<span className="material-symbols">person_raised_hand</span>
<H lvl={3} margin={false} padding={false}>
{heading}
</H>
@@ -1,6 +1,5 @@
import { css } from '@/styled-system/css'
import { ReactNode } from 'react'
import { Icon } from '@/primitives'
type RowPosition = 'first' | 'middle' | 'last' | 'single'
@@ -46,7 +45,7 @@ export const RowWrapper = ({
})}
>
{/* fixme - doesn't handle properly material-symbols */}
<Icon name={iconName} />
<span className="material-icons">{iconName}</span>
</div>
<div
className={css({
@@ -112,8 +112,8 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
square
size={'sm'}
onPress={copyRoomUrlToClipboard}
aria-label={isRoomUrlCopied ? t('copied') : t('copyUrl')}
tooltip={isRoomUrlCopied ? t('copied') : t('copyUrl')}
aria-label={t('copyUrl')}
tooltip={t('copyUrl')}
>
{isRoomUrlCopied ? (
<RiCheckLine aria-hidden="true" />
@@ -138,12 +138,11 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
{formatPinCode(roomData?.pin_code)}
</Text>
</div>
<Button
variant={isCopied ? 'success' : 'secondaryText'}
size="sm"
fullWidth
aria-label={isCopied ? t('copied') : t('copy')}
aria-label={t('copy')}
style={{
justifyContent: 'start',
}}
@@ -174,7 +173,7 @@ export const InviteDialog = (props: Omit<DialogProps, 'title'>) => {
<Button
variant={isCopied ? 'success' : 'tertiary'}
fullWidth
aria-label={isCopied ? t('copied') : t('copy')}
aria-label={t('copy')}
onPress={copyRoomToClipboard}
data-attr="share-dialog-copy"
>
@@ -5,9 +5,6 @@ import { css } from '@/styled-system/css'
import { Participant } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { accessibilityStore } from '@/stores/accessibility'
import { useSnapshot } from 'valtio'
export const ANIMATION_DURATION = 3000
export const ANIMATION_DISTANCE = 300
@@ -143,53 +140,11 @@ export function ReactionPortal({
)
}
export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { announceReactions } = useSnapshot(accessibilityStore)
const [announcement, setAnnouncement] = useState<string | null>(null)
const [lastAnnouncedId, setLastAnnouncedId] = useState<number | null>(null)
const latestReaction =
reactions.length > 0 ? reactions[reactions.length - 1] : undefined
useEffect(() => {
if (!announceReactions) {
setAnnouncement(null)
return
}
if (!latestReaction) return
const isNewReaction = latestReaction.id !== lastAnnouncedId
if (!isNewReaction) return
const emojiLabel = getEmojiLabel(latestReaction.emoji, t)
const participantName = latestReaction.participant?.isLocal
? t('you')
: latestReaction.participant?.name?.trim() ||
t('someone', { defaultValue: 'Someone' })
setAnnouncement(t('announce', { name: participantName, emoji: emojiLabel }))
setLastAnnouncedId(latestReaction.id)
const timer = setTimeout(() => setAnnouncement(null), 1200)
return () => clearTimeout(timer)
}, [latestReaction, lastAnnouncedId, announceReactions, t])
return (
<>
{reactions.map((instance) => (
<ReactionPortal
key={instance.id}
emoji={instance.emoji}
participant={instance.participant}
/>
))}
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement ?? ''}
</div>
</>
)
}
export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) =>
reactions.map((instance) => (
<ReactionPortal
key={instance.id}
emoji={instance.emoji}
participant={instance.participant}
/>
))
@@ -13,7 +13,6 @@ import { Effects } from './effects/Effects'
import { Admin } from './Admin'
import { Tools } from './Tools'
import { Info } from './Info'
import { HStack } from '@/styled-system/jsx'
type StyledSidePanelProps = {
title: string
@@ -24,7 +23,6 @@ type StyledSidePanelProps = {
closeButtonTooltip: string
isSubmenu: boolean
onBack: () => void
backButtonLabel: string
}
const StyledSidePanel = ({
@@ -36,7 +34,6 @@ const StyledSidePanel = ({
closeButtonTooltip,
isSubmenu = false,
onBack,
backButtonLabel,
}: StyledSidePanelProps) => (
<aside
className={css({
@@ -66,34 +63,31 @@ const StyledSidePanel = ({
aria-hidden={isClosed}
aria-label={ariaLabel}
>
<HStack alignItems="center">
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
>
{isSubmenu && (
<Button
variant="secondaryText"
size="sm"
square
className={css({ marginRight: '0.5rem', marginLeft: '1rem' })}
aria-label={backButtonLabel}
className={css({ marginRight: '0.5rem' })}
onPress={onBack}
>
<RiArrowLeftLine size={20} aria-hidden="true" />
<RiArrowLeftLine size={20} />
</Button>
)}
<Heading
slot="title"
level={1}
className={text({ variant: 'h2' })}
style={{
paddingLeft: isSubmenu ? 0 : '1.5rem',
paddingTop: '1rem',
display: isClosed ? 'none' : 'flex',
justifyContent: 'start',
alignItems: 'center',
}}
>
{title}
</Heading>
</HStack>
{title}
</Heading>
<Div
position="absolute"
top="5"
@@ -135,6 +129,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null}
</div>
)
export const SidePanel = () => {
const {
activePanelId,
@@ -163,7 +158,6 @@ export const SidePanel = () => {
})}
isClosed={!isSidePanelOpen}
isSubmenu={isSubPanelOpen}
backButtonLabel={t('backToTools')}
onBack={() => (layoutStore.activeSubPanelId = null)}
>
<Panel isOpen={isParticipantsOpen}>
@@ -1,4 +1,4 @@
import { A, Div, Icon, Text } from '@/primitives'
import { A, Div, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
@@ -87,7 +87,7 @@ const ToolButton = ({
alignItems: 'center',
})}
>
<Icon type="symbols" name="chevron_forward" />
<span className="material-symbols">chevron_forward</span>
</div>
</RACButton>
)
@@ -163,7 +163,7 @@ export const Tools = () => {
</Text>
{isTranscriptEnabled && (
<ToolButton
icon={<Icon type="symbols" name="speech_to_text" />}
icon={<span className="material-symbols">speech_to_text</span>}
title={t('tools.transcript.title')}
description={t('tools.transcript.body')}
onPress={() => openTranscript()}
@@ -171,7 +171,7 @@ export const Tools = () => {
)}
{isScreenRecordingEnabled && (
<ToolButton
icon={<Icon type="symbols" name="mode_standby" />}
icon={<span className="material-symbols">mode_standby</span>}
title={t('tools.screenRecording.title')}
description={t('tools.screenRecording.body')}
onPress={() => openScreenRecording()}
@@ -1,7 +1,6 @@
import { useTranslation } from 'react-i18next'
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
import { css } from '@/styled-system/css'
import { useParticipants } from '@livekit/components-react'
import { useSidePanel } from '../../../hooks/useSidePanel'
@@ -20,8 +19,6 @@ export const ParticipantsToggle = ({
*/
const participants = useParticipants()
const numParticipants = participants?.length
const announcedCount =
numParticipants && numParticipants > 0 ? numParticipants : 1
const { isParticipantsOpen, toggleParticipants } = useSidePanel()
@@ -34,24 +31,21 @@ export const ParticipantsToggle = ({
display: 'inline-block',
})}
>
<VisualOnlyTooltip tooltip={t(tooltipLabel)}>
<ToggleButton
square
variant="primaryTextDark"
aria-label={`${t(tooltipLabel)}. ${t('count', {
count: announcedCount,
})}.`}
isSelected={isParticipantsOpen}
onPress={(e) => {
toggleParticipants()
onPress?.(e)
}}
data-attr={`controls-participants-${tooltipLabel}`}
{...props}
>
<RiGroupLine />
</ToggleButton>
</VisualOnlyTooltip>
<ToggleButton
square
variant="primaryTextDark"
aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)}
isSelected={isParticipantsOpen}
onPress={(e) => {
toggleParticipants()
onPress?.(e)
}}
data-attr={`controls-participants-${tooltipLabel}`}
{...props}
>
<RiGroupLine />
</ToggleButton>
<div
className={css({
position: 'absolute',
@@ -10,7 +10,6 @@ import {
ANIMATION_DURATION,
ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal'
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { Toolbar as RACToolbar } from 'react-aria-components'
import { Participant } from 'livekit-client'
import useRateLimiter from '@/hooks/useRateLimiter'
@@ -146,7 +145,7 @@ export const ReactionsToggle = () => {
<Button
key={index}
onPress={() => debouncedSendReaction(emoji)}
aria-label={t('send', { emoji: getEmojiLabel(emoji, t) })}
aria-label={t('send', { emoji })}
variant="primaryTextDark"
size="sm"
square
@@ -1,24 +0,0 @@
import { useTranslation } from 'react-i18next'
export const getEmojiLabel = (
emoji: string,
t: ReturnType<typeof useTranslation>['t']
) => {
const emojiLabels: Record<string, string> = {
'thumbs-up': t('emojis.thumbs-up', { defaultValue: 'thumbs up' }),
'thumbs-down': t('emojis.thumbs-down', { defaultValue: 'thumbs down' }),
'clapping-hands': t('emojis.clapping-hands', {
defaultValue: 'clapping hands',
}),
'red-heart': t('emojis.red-heart', { defaultValue: 'red heart' }),
'face-with-tears-of-joy': t('emojis.face-with-tears-of-joy', {
defaultValue: 'face with tears of joy',
}),
'face-with-open-mouth': t('emojis.face-with-open-mouth', {
defaultValue: 'surprised face',
}),
'party-popper': t('emojis.party-popper', { defaultValue: 'party popper' }),
'folded-hands': t('emojis.folded-hands', { defaultValue: 'folded hands' }),
}
return emojiLabels[emoji] ?? emoji
}
@@ -22,11 +22,6 @@ const Heading = styled('h1', {
},
})
enum DisconnectReasonKey {
DuplicateIdentity = 'duplicateIdentity',
ParticipantRemoved = 'participantRemoved',
}
export const FeedbackRoute = () => {
const { t } = useTranslation('rooms')
const [, setLocation] = useLocation()
@@ -37,25 +32,21 @@ export const FeedbackRoute = () => {
if (!state?.reason) return
switch (state.reason) {
case DisconnectReason.DUPLICATE_IDENTITY:
return DisconnectReasonKey.DuplicateIdentity
return 'duplicateIdentity'
case DisconnectReason.PARTICIPANT_REMOVED:
return DisconnectReasonKey.ParticipantRemoved
return 'participantRemoved'
}
}, [])
const showBackButton = reasonKey !== DisconnectReasonKey.ParticipantRemoved
return (
<Screen layout="centered" footer={false}>
<Center>
<VStack>
<Heading>{t(`feedback.heading.${reasonKey || 'normal'}`)}</Heading>
<HStack>
{showBackButton && (
<Button variant="secondary" onPress={() => window.history.back()}>
{t('feedback.back')}
</Button>
)}
<Button variant="secondary" onPress={() => window.history.back()}>
{t('feedback.back')}
</Button>
<Button variant="primary" onPress={() => setLocation('/')}>
{t('feedback.home')}
</Button>
@@ -2,7 +2,6 @@ import { Dialog, type DialogProps } from '@/primitives'
import { Tab, Tabs, TabList } from '@/primitives/Tabs.tsx'
import { css } from '@/styled-system/css'
import { text } from '@/primitives/Text.tsx'
import { Icon } from '@/primitives/Icon'
import { Heading } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import {
@@ -11,7 +10,6 @@ import {
RiSettings3Line,
RiSpeakerLine,
RiVideoOnLine,
RiEyeLine,
} from '@remixicon/react'
import { AccountTab } from './tabs/AccountTab'
import { NotificationsTab } from './tabs/NotificationsTab'
@@ -23,7 +21,6 @@ import { useRef } from 'react'
import { useMediaQuery } from '@/features/rooms/livekit/hooks/useMediaQuery'
import { SettingsDialogExtendedKey } from '@/features/settings/type'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import AccessibilityTab from './tabs/AccessibilityTab'
const tabsStyle = css({
maxHeight: '40.625rem', // fixme size copied from meet settings modal
@@ -109,16 +106,11 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
</Tab>
{isAdminOrOwner && (
<Tab icon highlight id={SettingsDialogExtendedKey.TRANSCRIPTION}>
<Icon type="symbols" name="speech_to_text" />
<span className="material-symbols">speech_to_text</span>
{isWideScreen &&
t(`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`)}
</Tab>
)}
<Tab icon highlight id={SettingsDialogExtendedKey.ACCESSIBILITY}>
<RiEyeLine />
{isWideScreen &&
t(`tabs.${SettingsDialogExtendedKey.ACCESSIBILITY}`)}
</Tab>
</TabList>
</div>
<div className={tabPanelContainerStyle}>
@@ -132,7 +124,6 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
<NotificationsTab id={SettingsDialogExtendedKey.NOTIFICATIONS} />
{/* Transcription tab won't be accessible if the tab is not active in the tab list */}
<TranscriptionTab id={SettingsDialogExtendedKey.TRANSCRIPTION} />
<AccessibilityTab id={SettingsDialogExtendedKey.ACCESSIBILITY} />
</div>
</Tabs>
</Dialog>
@@ -1,40 +0,0 @@
import { Field, H } from '@/primitives'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import { accessibilityStore } from '@/stores/accessibility'
export type AccessibilityTabProps = Pick<TabPanelProps, 'id'>
export const AccessibilityTab = ({ id }: AccessibilityTabProps) => {
const { t } = useTranslation('settings')
const snap = useSnapshot(accessibilityStore)
return (
<TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('tabs.accessibility')}</H>
<ul
className={css({
display: 'flex',
flexDirection: 'column',
gap: '1rem',
})}
>
<li>
<Field
type="switch"
label={t('accessibility.announceReactions.label')}
isSelected={snap.announceReactions}
onChange={(value) => {
accessibilityStore.announceReactions = value
}}
wrapperProps={{ noMargin: true, fullWidth: true }}
/>
</li>
</ul>
</TabPanel>
)
}
export default AccessibilityTab
@@ -5,5 +5,4 @@ export enum SettingsDialogExtendedKey {
GENERAL = 'general',
NOTIFICATIONS = 'notifications',
TRANSCRIPTION = 'transcription',
ACCESSIBILITY = 'accessibility',
}
+6 -3
View File
@@ -44,15 +44,18 @@
},
"slide1": {
"title": "Wechseln Sie zur Einfachheit. Testen Sie uns jetzt!",
"body": "Entdecken Sie eine intuitive und zugängliche Lösung, entwickelt für alle Mitarbeitenden im öffentlichen Dienst, ihre Partner und viele weitere."
"body": "Entdecken Sie eine intuitive und zugängliche Lösung, entwickelt für alle Mitarbeitenden im öffentlichen Dienst, ihre Partner und viele weitere.",
"imgAlt": "Illustration einer benutzerfreundlichen und barrierefreien Kollaborationsplattform"
},
"slide2": {
"title": "Gruppenanrufe ohne Einschränkungen",
"body": "Unbegrenzte Meeting-Dauer mit flüssiger, hochwertiger Kommunikation unabhängig von der Gruppengröße."
"body": "Unbegrenzte Meeting-Dauer mit flüssiger, hochwertiger Kommunikation unabhängig von der Gruppengröße.",
"imgAlt": "Bild eines virtuellen Meetings mit mehreren nahtlos zusammenarbeitenden Teilnehmenden"
},
"slide3": {
"title": "Verwandeln Sie Ihre Meetings mit KI",
"body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta jetzt testen!"
"body": "Erhalten Sie präzise und verwertbare Transkripte zur Steigerung Ihrer Produktivität. Funktion in der Beta jetzt testen!",
"imgAlt": "Illustration KI-gestützter Notizen in einem virtuellen Meeting"
}
}
}
+2 -16
View File
@@ -192,9 +192,7 @@
"leave": "Verlassen",
"participants": {
"open": "Alle ausblenden",
"closed": "Alle anzeigen",
"count_one": "{{count}} Teilnehmer",
"count_other": "{{count}} Teilnehmer"
"closed": "Alle anzeigen"
},
"tools": {
"open": "Weitere Tools ausblenden",
@@ -208,18 +206,7 @@
"reactions": {
"button": "Reaktion senden",
"send": "Reaktion {{emoji}} senden",
"announce": "{{name}} : {{emoji}}",
"you": "Sie",
"emojis": {
"thumbs-up": "Daumen hoch",
"thumbs-down": "Daumen runter",
"clapping-hands": "Klatschende Hände",
"red-heart": "rotes Herz",
"face-with-tears-of-joy": "Gesicht mit Freudentränen",
"face-with-open-mouth": "überraschter Gesichtsausdruck",
"party-popper": "Party-Popper",
"folded-hands": "gefaltete Hände"
}
"you": "Sie"
}
},
"options": {
@@ -290,7 +277,6 @@
},
"sidePanel": {
"ariaLabel": "Seitenleiste",
"backToTools": "Zurück zu den Meeting-Tools",
"heading": {
"participants": "Teilnehmer",
"effects": "Effekte",
+6 -3
View File
@@ -44,15 +44,18 @@
},
"slide1": {
"title": "Make the switch to simplicity. Try us now!",
"body": "Discover an intuitive and accessible solution, designed for all public agents, their partners, and much more."
"body": "Discover an intuitive and accessible solution, designed for all public agents, their partners, and much more.",
"imgAlt": "Illustration of a user-friendly and accessible collaboration platform"
},
"slide2": {
"title": "Host group calls without limits",
"body": "Unlimited time meetings, with smooth and high-quality communication, no matter the group size."
"body": "Unlimited time meetings, with smooth and high-quality communication, no matter the group size.",
"imgAlt": "Image of a virtual meeting with multiple participants collaborating seamlessly"
},
"slide3": {
"title": "Transform your meetings with AI",
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!"
"body": "Get accurate and actionable transcripts to boost your productivity. Feature in beta—try it now!",
"imgAlt": "Illustration of AI-powered note-taking in a virtual meeting"
}
}
}
+2 -16
View File
@@ -192,9 +192,7 @@
"leave": "Leave",
"participants": {
"open": "Hide everyone",
"closed": "See everyone",
"count_one": "{{count}} participant",
"count_other": "{{count}} participants"
"closed": "See everyone"
},
"tools": {
"open": "Hide more tools",
@@ -208,18 +206,7 @@
"reactions": {
"button": "Send reaction",
"send": "Send reaction {{emoji}}",
"announce": "{{name}} : {{emoji}}",
"you": "you",
"emojis": {
"thumbs-up": "thumbs up",
"thumbs-down": "thumbs down",
"clapping-hands": "clapping hands",
"red-heart": "red heart",
"face-with-tears-of-joy": "face with tears of joy",
"face-with-open-mouth": "surprised face",
"party-popper": "party popper",
"folded-hands": "folded hands"
}
"you": "you"
}
},
"options": {
@@ -290,7 +277,6 @@
},
"sidePanel": {
"ariaLabel": "Sidepanel",
"backToTools": "Back to meeting tools",
"heading": {
"participants": "Participants",
"effects": "Backgrounds and Effects",
@@ -108,18 +108,12 @@
"label": "Language"
},
"settingsButtonLabel": "Settings",
"accessibility": {
"announceReactions": {
"label": "Announce reactions aloud"
}
},
"tabs": {
"account": "Profile",
"audio": "Audio",
"video": "Video",
"general": "General",
"notifications": "Notifications",
"accessibility": "Accessibility",
"transcription": "Transcription"
}
}
+6 -3
View File
@@ -44,15 +44,18 @@
},
"slide1": {
"title": "Passez à la simplicité. Essayez-nous dès maintenant !",
"body": "Découvrez une solution intuitive et accessible, conçue pour tous les agents publics et leurs partenaires, et bien plus encore."
"body": "Découvrez une solution intuitive et accessible, conçue pour tous les agents publics et leurs partenaires, et bien plus encore.",
"imgAlt": "Illustration d'une plateforme de collaboration simple et accessible"
},
"slide2": {
"title": "Organisez des appels de groupe sans limite",
"body": "Réunions sans limite de temps, avec une communication fluide et de haute qualité, quel que soit le nombre."
"body": "Réunions sans limite de temps, avec une communication fluide et de haute qualité, quel que soit le nombre.",
"imgAlt": "Image d'une réunion virtuelle avec plusieurs participants collaborant efficacement"
},
"slide3": {
"title": "Transformez vos réunions avec l'IA",
"body": "Obtenez des transcriptions précises et actionnables, pour booster votre productivité. Fonctionnalité en beta, essayez-la maintenant !"
"body": "Obtenez des transcriptions précises et actionnables, pour booster votre productivité. Fonctionnalité en beta, essayez-la maintenant !",
"imgAlt": "Illustration de prise de notes assistée par l'IA dans une réunion virtuelle"
}
}
}
+2 -16
View File
@@ -192,9 +192,7 @@
"leave": "Quitter",
"participants": {
"open": "Masquer les participants",
"closed": "Afficher les participants",
"count_one": "{{count}} participant",
"count_other": "{{count}} participants"
"closed": "Afficher les participants"
},
"tools": {
"open": "Masquer les outils de réunion",
@@ -208,18 +206,7 @@
"reactions": {
"button": "Envoyer une réaction",
"send": "Envoyer la réaction {{emoji}}",
"announce": "{{name}} : {{emoji}}",
"you": "vous",
"emojis": {
"thumbs-up": "pouce levé",
"thumbs-down": "pouce baissé",
"clapping-hands": "applaudissements",
"red-heart": "cœur",
"face-with-tears-of-joy": "visage qui rit",
"face-with-open-mouth": "visage étonné",
"party-popper": "confettis",
"folded-hands": "mains jointes"
}
"you": "vous"
}
},
"options": {
@@ -290,7 +277,6 @@
},
"sidePanel": {
"ariaLabel": "Panneau latéral",
"backToTools": "Retour aux outils de réunion",
"heading": {
"participants": "Participants",
"effects": "Arrière-plans et effets",
@@ -108,18 +108,12 @@
"label": "Langue de l'application"
},
"settingsButtonLabel": "Paramètres",
"accessibility": {
"announceReactions": {
"label": "Vocaliser les réactions"
}
},
"tabs": {
"account": "Profil",
"audio": "Audio",
"video": "Vidéo",
"general": "Général",
"notifications": "Notifications",
"accessibility": "Accessibilité",
"transcription": "Transcription"
}
}
+6 -3
View File
@@ -44,15 +44,18 @@
},
"slide1": {
"title": "Stap over op eenvoud. Probeer ons nu!",
"body": "Ontdek een intuïtieve en toegankelijke oplossing, ontworpen voor alle openbare partijen, hun partners en nog veel meer."
"body": "Ontdek een intuïtieve en toegankelijke oplossing, ontworpen voor alle openbare partijen, hun partners en nog veel meer.",
"imgAlt": "Illustratie van een gebruiksvriendelijk en toegankelijk samenwerkingsplatform"
},
"slide2": {
"title": "Houdt groepsgesprekken zonder limieten",
"body": "Vergaderingen van onbeperkte lengte, met soepele en hoogwaardige communicatie, ongeacht de groepsgrootte."
"body": "Vergaderingen van onbeperkte lengte, met soepele en hoogwaardige communicatie, ongeacht de groepsgrootte.",
"imgAlt": "Afbeelding van een virtuele ontmoeting met meerdere deelnemers die naadloos samenwerken"
},
"slide3": {
"title": "Transformeer uw vergaderingen met AI",
"body": "Krijg nauwkeurige en bruikbare transcripties om uw productiviteit te stimuleren. Deze mogelijkheid is in bèta, probeer het nu!"
"body": "Krijg nauwkeurige en bruikbare transcripties om uw productiviteit te stimuleren. Deze mogelijkheid is in bèta, probeer het nu!",
"imgAlt": "Illustratie van AI-aangedreven notitie in een virtuele vergadering"
}
}
}
+2 -16
View File
@@ -192,9 +192,7 @@
"leave": "Vertrekken",
"participants": {
"open": "Verberg iedereen",
"closed": "Toon iedereen",
"count_one": "{{count}} deelnemer",
"count_other": "{{count}} deelnemers"
"closed": "Toon iedereen"
},
"tools": {
"open": "Meer tools verbergen",
@@ -208,18 +206,7 @@
"reactions": {
"button": "Stuur reactie",
"send": "Stuur reactie {{emoji}}",
"announce": "{{name}} : {{emoji}}",
"you": "U",
"emojis": {
"thumbs-up": "duim omhoog",
"thumbs-down": "duim omlaag",
"clapping-hands": "applaudisserende handen",
"red-heart": "rood hart",
"face-with-tears-of-joy": "gezicht met tranen van vreugde",
"face-with-open-mouth": "verrast gezicht",
"party-popper": "feestknaller",
"folded-hands": "gevouwen handen"
}
"you": "U"
}
},
"options": {
@@ -290,7 +277,6 @@
},
"sidePanel": {
"ariaLabel": "Zijbalk",
"backToTools": "Terug naar vergadertools",
"heading": {
"participants": "Deelnemers",
"effects": "Effecten",
-71
View File
@@ -1,71 +0,0 @@
import { cva, RecipeVariantProps } from '@/styled-system/css'
import { ComponentPropsWithoutRef } from 'react'
const iconRecipe = cva({
base: {
fontWeight: 'normal',
fontStyle: 'normal',
display: 'inline-block',
lineHeight: 1,
textTransform: 'none',
letterSpacing: 'normal',
wordWrap: 'normal',
whiteSpace: 'nowrap',
direction: 'ltr',
},
variants: {
type: {
icons: {
fontFamily: 'Material Icons Outlined',
webkitFontSmoothing: 'antialiased',
mozOsxFontSmoothing: 'grayscale',
textRendering: 'optimizeLegibility',
fontFeatureSettings: '"liga"',
},
symbols: {
fontFamily: 'Material Symbols Outlined Variable',
},
},
size: {
sm: {
fontSize: '18px',
},
md: {
fontSize: '24px',
},
lg: {
fontSize: '32px',
},
xl: {
fontSize: '40px',
},
},
},
defaultVariants: {
type: 'icons',
size: 'md',
},
})
export type IconRecipeProps = RecipeVariantProps<typeof iconRecipe>
export type IconProps = IconRecipeProps &
ComponentPropsWithoutRef<'span'> & {
name: string
}
export const Icon = ({ name, ...props }: IconProps) => {
const [variantProps, componentProps] = iconRecipe.splitVariantProps(props)
const { className, ...remainingComponentProps } = componentProps
return (
<span
translate="no"
aria-hidden="true"
className={[iconRecipe(variantProps), className].join(' ')}
{...remainingComponentProps}
>
{name}
</span>
)
}
+1 -2
View File
@@ -112,8 +112,7 @@ export const buttonRecipe = cva({
},
transition: 'box-shadow 0.2s ease-in-out',
'&[data-selected]': {
boxShadow:
'0 0 0 3px token(colors.primary.600) inset, 0 0 0 5px white inset',
boxShadow: 'token(colors.primary.400) 0px 0px 0px 3px inset',
},
'&[data-disabled]': {
backgroundColor: 'greyscale.100',
-1
View File
@@ -30,4 +30,3 @@ export { Ul } from './Ul'
export { VerticallyOffCenter } from './VerticallyOffCenter'
export { TextArea } from './TextArea'
export { Switch } from './Switch'
export { Icon } from './Icon'
-76
View File
@@ -1,76 +0,0 @@
import { proxy, subscribe } from 'valtio'
import { STORAGE_KEYS } from '@/utils/storageKeys'
import { deserializeToProxyMap } from '@/utils/valtio'
type AccessibilityState = {
announceReactions: boolean
}
const DEFAULT_STATE: AccessibilityState = {
announceReactions: false,
}
function getAccessibilityState(): AccessibilityState {
try {
const stored = localStorage.getItem(STORAGE_KEYS.ACCESSIBILITY)
if (stored) {
const parsed = JSON.parse(stored)
return {
...DEFAULT_STATE,
...parsed,
announceReactions:
typeof parsed.announceReactions === 'boolean'
? parsed.announceReactions
: DEFAULT_STATE.announceReactions,
}
}
// Legacy migration: if the setting was previously stored in notifications
const legacy = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS)
if (legacy) {
try {
const parsedLegacy = JSON.parse(legacy, deserializeToProxyMap)
if (typeof parsedLegacy?.announceReactions === 'boolean') {
const migratedState: AccessibilityState = {
...DEFAULT_STATE,
...parsedLegacy,
announceReactions: parsedLegacy.announceReactions,
}
try {
localStorage.setItem(
STORAGE_KEYS.ACCESSIBILITY,
JSON.stringify(migratedState)
)
localStorage.removeItem(STORAGE_KEYS.NOTIFICATIONS)
} catch {
// ignore persistence issues during migration
}
return migratedState
}
} catch {
// ignore legacy parsing issues
}
}
return DEFAULT_STATE
} catch (error: unknown) {
console.error(
'[AccessibilityStore] Failed to parse stored settings:',
error
)
return DEFAULT_STATE
}
}
export const accessibilityStore = proxy<AccessibilityState>(
getAccessibilityState()
)
subscribe(accessibilityStore, () => {
localStorage.setItem(
STORAGE_KEYS.ACCESSIBILITY,
JSON.stringify(accessibilityStore)
)
})
-14
View File
@@ -1,14 +0,0 @@
import { css } from '@/styled-system/css'
// Reusable visually hidden style for screen-reader-only content
export const srOnly = css({
position: 'absolute',
width: '1px',
height: '1px',
padding: 0,
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: 0,
})
+27
View File
@@ -0,0 +1,27 @@
@import '@fontsource/material-icons-outlined';
.material-icons,
.material-symbols {
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'liga';
}
.material-icons {
font-family: 'Material Icons Outlined';
}
.material-symbols {
font-family: 'Material Symbols Outlined Variable';
}
+1 -33
View File
@@ -1,4 +1,5 @@
@import './livekit.css';
@import './icons.css';
@layer reset, base, tokens, recipes, utilities;
html,
body,
@@ -53,36 +54,3 @@ body:has(.lk-video-conference) #crisp-chatbox > * > div[role='button'] {
transform: translateY(0);
}
}
/* Generated using the Advanced installation guidelines from
* https://fontsource.org/fonts/material-icons-outlined/install
*/
@font-face {
font-family: 'Material Icons Outlined';
font-style: normal;
font-display: block;
font-weight: 400;
src: url(@fontsource/material-icons-outlined/files/material-icons-outlined-latin-400-normal.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Generated using the Advanced installation guidelines from
* https://fontsource.org/fonts/material-symbols-outlined/install
*/
/* material-symbols-outlined-latin-wght-normal */
@font-face {
font-family: 'Material Symbols Outlined Variable';
font-style: normal;
font-display: block;
font-weight: 100 700;
src: url(@fontsource-variable/material-symbols-outlined/files/material-symbols-outlined-latin-wght-normal.woff2)
format('woff2-variations');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
-1
View File
@@ -4,5 +4,4 @@
export const STORAGE_KEYS = {
NOTIFICATIONS: 'app_notification_settings',
USER_PREFERENCES: 'app_user_preferences',
ACCESSIBILITY: 'app_accessibility_settings',
} as const
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "1.3.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "1.3.0",
"version": "1.2.0",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "1.3.0",
"version": "1.2.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "1.3.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "1.3.0",
"version": "1.2.0",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "1.3.0",
"version": "1.2.0",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "1.3.0"
version = "1.2.0"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",