Compare commits

..

26 Commits

Author SHA1 Message Date
Jacques ROUSSEL 2cd4a6efa8 (helm) add pdbs to deployments
In order to avoid a service interruption during a Kubernetes (k8s)
upgrade, we add a Pod Disruption Budget (PDB) to deployments.
2025-02-12 11:54:08 +01:00
lebaudantoine b5037db685 🔒️(docker) patch libssl3 and libcrypto3 to address CVE-2024-12797
Added temporary root privileges to update OpenSSL libraries. Upgrades libssl3
and libcrypto3 to 3.3.3-r0 to fix HIGH severity vulnerability. Properly
switches back to nginx user after updates. Maintains unprivileged execution
while addressing security concern affecting RFC7250 Raw Public Keys
authentication.

Security: CVE-2024-12797
2025-02-12 11:52:40 +01:00
lebaudantoine 19804d2e3f (frontend) add sound notification for unread messages
Add togglable audio alerts when new chat messages arrive,
allowing users to customize their notification preferences.
2025-02-12 11:52:40 +01:00
lebaudantoine a979f05549 🐛(frontend) preserve notification preferences while merging new types
When loading notification settings from localStorage,
keep user preferences for existing notification types while adding
new notification types with default values.

If a notification type is removed, make sure to get rid of it.
My initial implementation wasn't future proof.
2025-02-12 11:52:40 +01:00
lebaudantoine 09a0496d25 ♻️(frontend) standardize toast notification durations
Introduce ToastDuration enum to replace magic numbers for notification
timeouts. Added semantic duration constants for different notification types
(alerts, messages, join/leave events). Improves maintainability and
consistency across toast implementations.
2025-02-12 11:52:40 +01:00
lebaudantoine da95e804a0 (frontend) add toast notifications for unread chat messages
Users frequently miss chat messages due to discrete visual indicators.
Implemented toast notifications showing sender name and message preview to
improve visibility. Added message tracking and auto-dismiss when chat panel
opens.

Remove the warning in handleDataReceived function, it was triggered by
chat message events.
2025-02-12 11:52:40 +01:00
lebaudantoine 591a3a5d8b (frontend) notify user when her was muted
Add notification system to handle user mute events. Implement extensible
design to support future notification types. Set notification duration to 3s.
2025-02-12 11:52:40 +01:00
lebaudantoine 30ed0da416 (frontend) introduce a new notification type
This notification alert the user when her was muted by another one.
Simple copywritting.
2025-02-12 11:52:40 +01:00
lebaudantoine bd4fcc2a5e (frontend) notify participant when her was muted
Make sure a participant is notified when another one muted her.
It will help display a notification to alert the participant.
2025-02-12 11:52:40 +01:00
lebaudantoine fc36ae8b49 🐛(frontend) fix useRoomData when a participant's name has changed
The participant's name in the query key prevented proper cache invalidation
when renamed. Since name changes don't affect query data, removed this
dependency.
2025-02-12 11:52:40 +01:00
lebaudantoine 87baca247b 🚨(frontend) fix warning component from uncontrolled to controlled
When the hand has never been raised yet, the value is undefined and
not false, which leads to a react aria warning.

It's a bad practice. Fix it.
2025-02-12 11:52:40 +01:00
lebaudantoine 42d3846660 ✏️(frontend) fix minor typo
Typo identified by Celine. No "e" in french.
2025-02-11 12:04:17 +01:00
lebaudantoine 2d393f9f70 (frontend) add controls for full screen screenshare
Requested by several users.
Inspired by Jitsi.

Yet imperfect implementation. Controls on video element of
a screenshare should be disabled.
2025-02-11 12:01:23 +01:00
lebaudantoine b4b4ff79d9 ♻️(frontend) refactor full screen to allow video args
useFullScreen hook is now generic, it allows passing a specific
video element to zoom in.

Needed to zoom a specific video track.
2025-02-11 12:01:23 +01:00
lebaudantoine 861244ce01 💄(frontend) update cursor on disabled button
When a button is disabled, it's misleading to show a pointer.
Update it to the default pointer.
2025-02-11 12:01:23 +01:00
lebaudantoine 6373593de3 🚸(frontend) offer additionnal controls on participant tile
On hover, based on participant's type (remove/local) offer an
appropriate action. Either applying effects on the local participant
video or muting the remote participant.

This is a huge enhancement in term of UX, nobody was finding these two
controls in the current menus, and though the features were not
implemented.
2025-02-11 12:01:23 +01:00
lebaudantoine 03f6e6519b 🩹(frontend) remove warning on modal without heading
The MuteAlertDialog was triggering accessibility warnings, lacking
a proper heading. Fix it.
2025-02-11 12:01:23 +01:00
lebaudantoine 716d40dd4e 🎨(frontend) avoid duplicating prefix in i18n's key
Simple enhancement. Avoid duplicating prefix.
2025-02-11 12:01:23 +01:00
lebaudantoine 17f8ec6319 🚸(frontend) refactor participant tile while being hovered
Inspired by GMeet. Make central actions available on a participant
tile when a user hover it.

This new interactive zone will be extended with more actions and
controls.
2025-02-11 12:01:23 +01:00
lebaudantoine 0b74cf96f2 ♻️(frontend) extract MuteAlertDialog in a dedicated component
Will be reused in actions displayed when a participant tile
is focused.
2025-02-11 12:01:23 +01:00
Jacques ROUSSEL 723b8718f9 🔐(helm) bump chart version
Bump chart version to publish a new one with evolution
2025-02-05 22:20:49 +01:00
Nathan Vasse 07a44dab36 (front) turn on camera when using effects
We want to automatically turn on the camera if its off when
enabling a new effect.
2025-02-05 11:57:00 +01:00
Nathan Vasse e47b027bbc (front) add loading state on effects component
useSyncAfterDelay allows to enable loading indicators only if the
loading takes more than a specific time. It prevent blinking
effect when the loading time is nearly instant.
2025-02-05 11:57:00 +01:00
Nathan Vasse d7a4f3946c (front) add Loader component
This component will be used for various purpose.
2025-02-05 11:57:00 +01:00
Jacques ROUSSEL 1b7523bbf1 💚(github) fix argocd notification
Use the right variable for webhook url
2025-02-05 11:53:56 +01:00
Jacques ROUSSEL 4326df4b6a 💚(github) fix argocd notification
Fix double simple quote issue on argocd notification job
2025-02-05 11:48:38 +01:00
35 changed files with 648 additions and 80 deletions
+2 -2
View File
@@ -127,5 +127,5 @@ jobs:
name: Call argocd github webhook
run: |
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${{ secrets.ARGOCD_WEBHOOK_SECRET }}'' | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_WEBHOOK_URL }}
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac "${{ secrets.ARGOCD_WEBHOOK_SECRET }}" | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}
+1 -3
View File
@@ -3,10 +3,8 @@ run-name: Release Chart
on:
push:
branches:
- 'main'
paths:
- ./src/helm/meet/**
- src/helm/meet/**
jobs:
release:
+1
View File
@@ -1,3 +1,4 @@
# Changelog
All notable changes to this project will be documented in this file.
+5
View File
@@ -34,6 +34,11 @@ RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
USER root
RUN apk update && apk upgrade libssl3 libcrypto3
USER nginx
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
@@ -1,16 +1,81 @@
import { useEffect } from 'react'
import { useRoomContext } from '@livekit/components-react'
import { Participant, RoomEvent } from 'livekit-client'
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { ToastProvider, toastQueue } from './components/ToastProvider'
import { NotificationType } from './NotificationType'
import { Div } from '@/primitives'
import { isMobileBrowser } from '@livekit/components-core'
import { ChatMessage, isMobileBrowser } from '@livekit/components-core'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
enum ToastDuration {
SHORT = 3000,
MEDIUM = 4000,
LONG = 5000,
}
const NotificationDuration = {
ALERT: ToastDuration.SHORT,
MESSAGE: ToastDuration.LONG,
PARTICIPANT_JOINED: ToastDuration.LONG,
HAND_RAISED: ToastDuration.LONG,
} as const
export const MainNotificationToast = () => {
const room = useRoomContext()
const { triggerNotificationSound } = useNotificationSound()
useEffect(() => {
const handleChatMessage = (
chatMessage: ChatMessage,
participant?: Participant | undefined
) => {
if (!participant || participant.isLocal) return
triggerNotificationSound(NotificationType.MessageReceived)
toastQueue.add(
{
participant: participant,
message: chatMessage.message,
type: NotificationType.MessageReceived,
},
{ timeout: NotificationDuration.MESSAGE }
)
}
room.on(RoomEvent.ChatMessage, handleChatMessage)
return () => {
room.off(RoomEvent.ChatMessage, handleChatMessage)
}
}, [room, triggerNotificationSound])
useEffect(() => {
const handleDataReceived = (
payload: Uint8Array,
participant?: RemoteParticipant
) => {
const decoder = new TextDecoder()
const notificationType = decoder.decode(payload)
if (!participant) return
switch (notificationType) {
case NotificationType.ParticipantMuted:
toastQueue.add(
{
participant,
type: NotificationType.ParticipantMuted,
},
{ timeout: NotificationDuration.ALERT }
)
break
default:
return
}
}
room.on(RoomEvent.DataReceived, handleDataReceived)
return () => {
room.off(RoomEvent.DataReceived, handleDataReceived)
}
}, [room])
useEffect(() => {
const showJoinNotification = (participant: Participant) => {
if (isMobileBrowser()) {
@@ -23,7 +88,7 @@ export const MainNotificationToast = () => {
type: NotificationType.ParticipantJoined,
},
{
timeout: 5000,
timeout: NotificationDuration.PARTICIPANT_JOINED,
}
)
}
@@ -82,7 +147,7 @@ export const MainNotificationToast = () => {
participant,
type: NotificationType.HandRaised,
},
{ timeout: 5000 }
{ timeout: NotificationDuration.HAND_RAISED }
)
}
}
@@ -1,5 +1,6 @@
export enum NotificationType {
ParticipantJoined = 'participantJoined',
HandRaised = 'handRaised',
// todo - implement message received notification
ParticipantMuted = 'participantMuted',
MessageReceived = 'messageReceived',
}
@@ -0,0 +1,74 @@
import { useToast } from '@react-aria/toast'
import { useEffect, useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { Text } from '@/primitives'
import { RiMessage2Line } from '@remixicon/react'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { Button as RACButton } from 'react-aria-components'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
export function ToastMessageReceived({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps } = useToast(props, state, ref)
const toast = props.toast
const participant = toast.content.participant
const message = toast.content.message
const { isChatOpen, toggleChat } = useSidePanel()
useEffect(() => {
if (isChatOpen) {
state.close(toast.key)
}
}, [isChatOpen, toast, state])
if (isChatOpen) return null
return (
<StyledToastContainer {...toastProps} ref={ref}>
<RACButton onPress={() => toggleChat()} aria-label={t('openChat')}>
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
padding: '14px',
gap: '0.75rem',
textAlign: 'start',
width: '150px',
md: {
width: '260px',
},
})}
>
<div
className={css({
display: 'flex',
flexDirection: 'row',
justifyContent: 'start',
gap: '0.5rem',
})}
>
<RiMessage2Line
size={20}
className={css({
color: 'primary.300',
marginTop: '3px',
})}
aria-hidden="true"
/>
<span>{participant.name}</span>
</div>
<Text margin={false} wrap={'pretty'} centered={false}>
{message}
</Text>
</div>
</RACButton>
</StyledToastContainer>
)
}
@@ -0,0 +1,26 @@
import { useToast } from '@react-aria/toast'
import { useRef } from 'react'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
export function ToastMuted({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications')
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const participant = props.toast.content.participant
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={0}
>
{t('muted', { name: participant.name })}
</HStack>
</StyledToastContainer>
)
}
@@ -5,7 +5,9 @@ import { useRef } from 'react'
import { NotificationType } from '../NotificationType'
import { ToastJoined } from './ToastJoined'
import { ToastData } from './ToastProvider'
import { ToastRaised } from '@/features/notifications/components/ToastRaised.tsx'
import { ToastRaised } from './ToastRaised'
import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -23,6 +25,14 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) {
if (toast.content?.type === NotificationType.HandRaised) {
return <ToastRaised key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.ParticipantMuted) {
return <ToastMuted key={toast.key} toast={toast} state={state} />
}
if (toast.content?.type === NotificationType.MessageReceived) {
return (
<ToastMessageReceived key={toast.key} toast={toast} state={state} />
)
}
return <Toast key={toast.key} toast={toast} state={state} />
})}
</div>
@@ -32,7 +32,7 @@ export const Conference = ({
useEffect(() => {
posthog.capture('visit-room', { slug: roomId })
}, [roomId])
const fetchKey = [keys.room, roomId, userConfig.username]
const fetchKey = [keys.room, roomId]
const {
mutateAsync: createRoom,
@@ -49,6 +49,7 @@ export const Conference = ({
isError: isFetchError,
data,
} = useQuery({
/* eslint-disable @tanstack/query/exhaustive-deps */
queryKey: fetchKey,
staleTime: 6 * 60 * 60 * 1000, // By default, LiveKit access tokens expire 6 hours after generation
initialData: initialRoomData,
@@ -3,11 +3,23 @@ import Source = Track.Source
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'
import { useRoomContext } from '@livekit/components-react'
import { NotificationType } from '@/features/notifications/NotificationType'
export const useMuteParticipant = () => {
const data = useRoomData()
const room = useRoomContext()
const muteParticipant = (participant: Participant) => {
const notifyParticipant = async (participant: Participant) => {
const encoder = new TextEncoder()
const data = encoder.encode(NotificationType.ParticipantMuted)
await room.localParticipant.publishData(data, {
reliable: true,
destinationIdentities: [participant.identity],
})
}
const muteParticipant = async (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
@@ -18,22 +30,33 @@ export const useMuteParticipant = () => {
if (!trackSid) {
throw new Error('Missing audio track')
}
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/MutePublishedTrack'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
muted: true,
track_sid: trackSid,
}),
}
)
try {
const response = await fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/MutePublishedTrack'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
muted: true,
track_sid: trackSid,
}),
}
)
await notifyParticipant(participant)
return response
} catch (error) {
console.error(
`Failed to mute participant ${participant.identity}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
return { muteParticipant }
}
@@ -0,0 +1,36 @@
import { useTranslation } from 'react-i18next'
import { Button, Dialog, P } from '@/primitives'
import { HStack } from '@/styled-system/jsx'
export const MuteAlertDialog = ({
isOpen,
onClose,
onSubmit,
name,
}: {
isOpen: boolean
onClose: () => void
onSubmit: () => void
name: string
}) => {
const { t } = useTranslation('rooms', {
keyPrefix: 'participants.muteParticipantAlert',
})
return (
<Dialog
isOpen={isOpen}
role="alertdialog"
aria-label={t('heading', { name })}
>
<P>{t('description', { name })}</P>
<HStack gap={1}>
<Button variant="text" size="sm" onPress={onClose}>
{t('cancel')}
</Button>
<Button variant="text" size="sm" onPress={onSubmit}>
{t('confirm')}
</Button>
</HStack>
</Dialog>
)
}
@@ -1,7 +1,6 @@
import {
AudioTrack,
ConnectionQualityIndicator,
FocusToggle,
LockLockedIcon,
ParticipantName,
ParticipantTileProps,
@@ -23,11 +22,12 @@ import {
TrackReferenceOrPlaceholder,
} from '@livekit/components-core'
import { Track } from 'livekit-client'
import { ParticipantPlaceholder } from '@/features/rooms/livekit/components/ParticipantPlaceholder'
import { RiHand } from '@remixicon/react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { useRaisedHand } from '../hooks/useRaisedHand'
import { HStack } from '@/styled-system/jsx'
import { MutedMicIndicator } from '@/features/rooms/livekit/components/MutedMicIndicator'
import { MutedMicIndicator } from './MutedMicIndicator'
import { ParticipantPlaceholder } from './ParticipantPlaceholder'
import { ParticipantTileFocus } from './ParticipantTileFocus'
export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
@@ -173,7 +173,9 @@ export const ParticipantTile: (
)}
</>
)}
{!disableMetadata && <FocusToggle trackRef={trackReference} />}
{!disableMetadata && (
<ParticipantTileFocus trackRef={trackReference} />
)}
</ParticipantContextIfNeeded>
</TrackRefContextIfNeeded>
</div>
@@ -0,0 +1,208 @@
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { Button } from '@/primitives'
import {
RiFullscreenLine,
RiImageCircleAiFill,
RiMicLine,
RiMicOffLine,
RiPushpin2Line,
RiUnpinLine,
} from '@remixicon/react'
import {
useFocusToggle,
useTrackMutedIndicator,
} from '@livekit/components-react'
import { useTranslation } from 'react-i18next'
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { useEffect, useState } from 'react'
import { useSidePanel } from '../hooks/useSidePanel'
import { useFullScreen } from '../hooks/useFullScreen'
import { Participant, Track } from 'livekit-client'
import { MuteAlertDialog } from './MuteAlertDialog'
import { useMuteParticipant } from '../api/muteParticipant'
const ZoomButton = ({
trackRef,
}: {
trackRef: TrackReferenceOrPlaceholder
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({
trackRef,
})
if (!isFullscreenAvailable) {
return
}
return (
<Button
size="sm"
variant="primaryTextDark"
square
tooltip={t('fullScreen')}
onPress={() => toggleFullScreen()}
>
<RiFullscreenLine />
</Button>
)
}
const FocusButton = ({
trackRef,
}: {
trackRef: TrackReferenceOrPlaceholder
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
const { mergedProps, inFocus } = useFocusToggle({
trackRef,
props: {},
})
return (
<Button
size="sm"
variant="primaryTextDark"
square
tooltip={inFocus ? t('pin.disable') : t('pin.enable')}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onPress={(event) => mergedProps?.onClick?.(event as any)}
>
{inFocus ? <RiUnpinLine /> : <RiPushpin2Line />}
</Button>
)
}
const EffectsButton = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
const { isEffectsOpen, toggleEffects } = useSidePanel()
return (
<Button
size={'sm'}
variant={'primaryTextDark'}
square
tooltip={t('effects')}
onPress={() => !isEffectsOpen && toggleEffects()}
>
<RiImageCircleAiFill />
</Button>
)
}
const MuteButton = ({ participant }: { participant: Participant }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
const { isMuted } = useTrackMutedIndicator({
participant: participant,
source: Track.Source.Microphone,
})
const { muteParticipant } = useMuteParticipant()
const [isAlertOpen, setIsAlertOpen] = useState(false)
const name = participant.name || participant.identity
return (
<>
<Button
isDisabled={isMuted}
size={'sm'}
variant={'primaryTextDark'}
square
onPress={() => setIsAlertOpen(true)}
tooltip={t('muteParticipant', { name })}
>
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
</Button>
<MuteAlertDialog
isOpen={isAlertOpen}
onSubmit={() =>
muteParticipant(participant).then(() => setIsAlertOpen(false))
}
onClose={() => setIsAlertOpen(false)}
name={name}
/>
</>
)
}
export const ParticipantTileFocus = ({
trackRef,
}: {
trackRef: TrackReferenceOrPlaceholder
}) => {
const [hovered, setHovered] = useState(false)
const [opacity, setOpacity] = useState(0)
useEffect(() => {
if (hovered) {
// Wait for next frame to ensure element is mounted
requestAnimationFrame(() => {
setOpacity(0.6)
})
} else {
setOpacity(0)
}
}, [hovered])
const participant = trackRef.participant
const isScreenShare = trackRef.source == Track.Source.ScreenShare
const isLocal = trackRef.participant.isLocal
return (
<div
className={css({
position: 'absolute',
left: '0',
top: '0',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
})}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{hovered && (
<div
className={css({
backgroundColor: 'primaryDark.50',
transition: 'opacity 200ms linear',
zIndex: 1,
borderRadius: '0.25rem',
display: 'flex',
_hover: {
opacity: '0.95 !important',
},
})}
style={{ opacity }}
>
<HStack
gap={0.5}
className={css({
padding: '0.5rem',
_hover: {
opacity: '1 !important',
},
})}
>
<FocusButton trackRef={trackRef} />
{!isScreenShare ? (
<>
{participant.isLocal ? (
<EffectsButton />
) : (
<MuteButton participant={participant} />
)}
</>
) : (
!isLocal && <ZoomButton trackRef={trackRef} />
)}
</HStack>
</div>
)}
</div>
)
}
@@ -7,7 +7,7 @@ import { useFullScreen } from '../../../hooks/useFullScreen'
export const FullScreenMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleFullScreen, isCurrentlyFullscreen, isFullscreenAvailable } =
useFullScreen()
useFullScreen({})
if (!isFullscreenAvailable) {
return null
@@ -13,36 +13,10 @@ import {
} from '@livekit/components-react'
import Source = Track.Source
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
import { Button, Dialog, P } from '@/primitives'
import { Button } from '@/primitives'
import { useState } from 'react'
import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant'
const MuteAlertDialog = ({
isOpen,
onClose,
onSubmit,
name,
}: {
isOpen: boolean
onClose: () => void
onSubmit: () => void
name: string
}) => {
const { t } = useTranslation('rooms')
return (
<Dialog isOpen={isOpen} role="alertdialog">
<P>{t('participants.muteParticipantAlert.description', { name })}</P>
<HStack gap={1}>
<Button variant="text" size="sm" onPress={onClose}>
{t('participants.muteParticipantAlert.cancel')}
</Button>
<Button variant="text" size="sm" onPress={onSubmit}>
{t('participants.muteParticipantAlert.confirm')}
</Button>
</HStack>
</Dialog>
)
}
import { MuteAlertDialog } from '../../MuteAlertDialog'
type MicIndicatorProps = {
participant: Participant
@@ -2,9 +2,32 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
export function useFullScreen({
trackRef,
}: {
trackRef?: TrackReferenceOrPlaceholder
}) {
const videoElement = useMemo(() => {
const elements = trackRef?.publication?.track?.attachedElements
if (!elements) return
// Find the visible video element
const likeKitElement = elements.find((el) =>
el.classList.contains('lk-participant-media-video')
)
if (!likeKitElement) {
console.warn('Could not find LiveKit-managed video element')
return elements[0] || null
}
return likeKitElement
}, [trackRef])
export function useFullScreen() {
const getIsFullscreen = () => {
return !!(
document.fullscreenElement ||
@@ -24,7 +47,7 @@ export function useFullScreen() {
const enterFullscreen = async () => {
try {
const docEl = document.documentElement
const docEl = videoElement || document.documentElement
if (docEl.requestFullscreen) {
await docEl.requestFullscreen()
} else if (docEl.webkitRequestFullscreen) {
@@ -19,5 +19,5 @@ export function useRaisedHand({ participant }: useRaisedHandProps) {
}
}
return { isHandRaised: parsedMetadata.raised, toggleRaisedHand }
return { isHandRaised: parsedMetadata.raised || false, toggleRaisedHand }
}
@@ -1,12 +1,10 @@
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
import { useRoomContext } from '@livekit/components-react'
import { useParams } from 'wouter'
import { keys } from '@/api/queryKeys'
import { queryClient } from '@/api/queryClient'
export const useRoomData = (): ApiRoom | undefined => {
const room = useRoomContext()
const { roomId } = useParams()
const queryKey = [keys.room, roomId, room.localParticipant.name]
const queryKey = [keys.room, roomId]
return queryClient.getQueryData<ApiRoom>(queryKey)
}
@@ -6,5 +6,7 @@
"raised": {
"description": "",
"cta": ""
}
},
"muted": "",
"openChat": ""
}
+10
View File
@@ -179,6 +179,7 @@
"muteYourself": "",
"muteParticipant": "",
"muteParticipantAlert": {
"heading": "",
"description": "",
"confirm": "",
"cancel": ""
@@ -189,5 +190,14 @@
},
"recording": {
"label": ""
},
"participantTileFocus": {
"pin": {
"enable": "",
"disable": ""
},
"effects": "",
"muteParticipant": "",
"fullScreen": ""
}
}
@@ -6,5 +6,7 @@
"raised": {
"description": "{{name}} has raised their hand.",
"cta": "Open waiting list"
}
},
"muted": "{{name}} has muted your microphone. No participant can hear you.",
"openChat": "Open chat"
}
+10
View File
@@ -178,6 +178,7 @@
"muteYourself": "Close your mic",
"muteParticipant": "Close the mic of {{name}}",
"muteParticipantAlert": {
"heading": "Mute {{name}}",
"description": "Mute {{name}} for all participants? {{name}} will be the only one who can unmute themselves.",
"confirm": "Mute",
"cancel": "Cancel"
@@ -188,5 +189,14 @@
},
"recording": {
"label": "Recording"
},
"participantTileFocus": {
"pin": {
"enable": "Pin",
"disable": "Unpin"
},
"effects": "Apply visual effects",
"muteParticipant": "Mute {{name}}",
"fullScreen": "Full screen"
}
}
@@ -6,5 +6,7 @@
"raised": {
"description": "{{name}} a levé la main.",
"cta": "Ouvrir la file d'attente"
}
},
"muted": "{{name}} a coupé votre micro. Aucun participant ne peut l'entendre.",
"openChat": "Ouvrir le chat"
}
+10
View File
@@ -178,6 +178,7 @@
"muteYourself": "Couper votre micro",
"muteParticipant": "Couper le micro de {{name}}",
"muteParticipantAlert": {
"heading": "Couper le micro de {{name}}",
"description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro",
"confirm": "Couper le micro",
"cancel": "Annuler"
@@ -188,5 +189,14 @@
},
"recording": {
"label": "Enregistrement"
},
"participantTileFocus": {
"pin": {
"enable": "Épingler",
"disable": "Annuler l'épinglage"
},
"effects": "Appliquer des effets",
"muteParticipant": "Couper le micro de {{name}}",
"fullScreen": "Plein écran"
}
}
+1 -1
View File
@@ -41,7 +41,7 @@
},
"settingsButtonLabel": "Paramètres",
"tabs": {
"account": "Profile",
"account": "Profil",
"audio": "Audio",
"general": "Général",
"notifications": "Notifications"
@@ -8,6 +8,9 @@ export const buttonRecipe = cva({
transition: 'background 200ms, outline 200ms, border-color 200ms',
cursor: 'pointer',
border: '1px solid transparent',
'&[data-disabled]': {
cursor: 'default',
},
},
variants: {
size: {
+16 -1
View File
@@ -14,6 +14,7 @@ const DEFAULT_STATE: State = {
new Map([
[NotificationType.ParticipantJoined, true],
[NotificationType.HandRaised, true],
[NotificationType.MessageReceived, true],
])
),
soundNotificationVolume: 0.1,
@@ -24,7 +25,21 @@ function getNotificationsState(): State {
const stored = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS)
if (!stored) return DEFAULT_STATE
const parsed = JSON.parse(stored, deserializeToProxyMap)
return parsed || DEFAULT_STATE
// Ensure all default notification types exist in the recovered state
return {
...DEFAULT_STATE,
...parsed,
soundNotifications: proxyMap(
new Map(
Array.from(DEFAULT_STATE.soundNotifications.keys()).map((key) => [
key,
parsed.soundNotifications.has(key)
? parsed.soundNotifications.get(key)
: DEFAULT_STATE.soundNotifications.get(key),
])
)
),
}
} catch (error: unknown) {
console.error(
'[NotificationsStore] Failed to parse stored settings:',
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.1
version: 0.0.3
+4
View File
@@ -82,6 +82,7 @@
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
### frontend
@@ -131,6 +132,7 @@
| `frontend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
| `frontend.extraVolumeMounts` | Additional volumes to mount on the frontend. | `[]` |
| `frontend.extraVolumes` | Additional volumes to mount on the frontend. | `[]` |
| `frontend.pdb.enabled` | Enable pdb on frontend | `true` |
### posthog
@@ -208,6 +210,7 @@
| `summary.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
| `summary.extraVolumeMounts` | Additional volumes to mount on the summary. | `[]` |
| `summary.extraVolumes` | Additional volumes to mount on the summary. | `[]` |
| `summary.pdb.enabled` | Enable pdb on summary | `true` |
### celery
@@ -255,3 +258,4 @@
| `celery.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
| `celery.extraVolumeMounts` | Additional volumes to mount on the celery. | `[]` |
| `celery.extraVolumes` | Additional volumes to mount on the celery. | `[]` |
| `celery.pdb.enabled` | Enable pdb on celery | `false` |
@@ -138,3 +138,16 @@ spec:
emptyDir: {}
{{- end }}
{{- end }}
---
{{ if .Values.backend.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
@@ -138,3 +138,16 @@ spec:
emptyDir: {}
{{- end }}
{{- end }}
---
{{ if .Values.celery.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
@@ -138,3 +138,16 @@ spec:
emptyDir: {}
{{- end }}
{{- end }}
---
{{ if .Values.frontend.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
@@ -138,3 +138,16 @@ spec:
emptyDir: {}
{{- end }}
{{- end }}
---
{{ if .Values.summary.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
+17 -4
View File
@@ -120,8 +120,8 @@ backend:
targetPort: 8000
annotations: {}
## @param backend.migrate.command backend migrate command
## @param backend.migrate.restartPolicy backend migrate job restart policy
## @param backend.migrate.command backend migrate command
## @param backend.migrate.restartPolicy backend migrate job restart policy
migrate:
command:
- "python"
@@ -130,8 +130,8 @@ backend:
- "--no-input"
restartPolicy: Never
## @param backend.createsuperuser.command backend migrate command
## @param backend.createsuperuser.restartPolicy backend migrate job restart policy
## @param backend.createsuperuser.command backend migrate command
## @param backend.createsuperuser.restartPolicy backend migrate job restart policy
createsuperuser:
command:
- "/bin/sh"
@@ -184,6 +184,9 @@ backend:
## @param backend.extraVolumes Additional volumes to mount on the backend.
extraVolumes: []
## @param backend.pdb.enabled Enable pdb on backend
pdb:
enabled: true
## @section frontend
@@ -279,6 +282,10 @@ frontend:
## @param frontend.extraVolumes Additional volumes to mount on the frontend.
extraVolumes: []
## @param frontend.pdb.enabled Enable pdb on frontend
pdb:
enabled: true
## @section posthog
posthog:
@@ -443,6 +450,9 @@ summary:
## @param summary.extraVolumes Additional volumes to mount on the summary.
extraVolumes: []
## @param summary.pdb.enabled Enable pdb on summary
pdb:
enabled: true
## @section celery
@@ -533,3 +543,6 @@ celery:
## @param celery.extraVolumes Additional volumes to mount on the celery.
extraVolumes: []
## @param celery.pdb.enabled Enable pdb on celery
pdb:
enabled: false