diff --git a/src/frontend/src/features/participants/components/ParticipantsCount.tsx b/src/frontend/src/features/participants/components/ParticipantsCount.tsx
new file mode 100644
index 00000000..fff36dcc
--- /dev/null
+++ b/src/frontend/src/features/participants/components/ParticipantsCount.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { useRemoteParticipants } from '@livekit/components-react'
+import { srOnly } from '@/styles/a11y'
+import { css } from '@/styled-system/css'
+import { RiInfinityLine } from '@remixicon/react'
+
+const badgeStyles = css({
+ position: 'absolute',
+ top: '-.25rem',
+ right: '-.25rem',
+ width: '1.25rem',
+ height: '1.25rem',
+ backgroundColor: 'gray',
+ borderRadius: '50%',
+ color: 'white',
+ fontSize: '0.75rem',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center',
+ zIndex: 1,
+ userSelect: 'none',
+})
+
+/**
+ * Isolated so participant join/leave events only re-render the badge,
+ * not the button, tooltip or shortcut registration.
+ */
+export const ParticipantsCount = React.memo(
+ ({ describedById }: { describedById: string }) => {
+ const { t } = useTranslation('rooms', {
+ keyPrefix: 'controls.participants',
+ })
+ const remoteParticipants = useRemoteParticipants({
+ updateOnlyOn: [],
+ })
+ const count = (remoteParticipants?.length ?? 0) + 1
+
+ return (
+ <>
+
+ {t('count', { count })}
+
+
+ {count < 100 ? count : }
+
+ >
+ )
+ }
+)
+ParticipantsCount.displayName = 'ParticipantsCount'
diff --git a/src/frontend/src/features/rooms/livekit/components/controls/ParticipantsToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/ParticipantsToggle.tsx
index 5f809198..8e874009 100644
--- a/src/frontend/src/features/rooms/livekit/components/controls/ParticipantsToggle.tsx
+++ b/src/frontend/src/features/rooms/livekit/components/controls/ParticipantsToggle.tsx
@@ -1,36 +1,22 @@
+import { useCallback, useId } from 'react'
import { useTranslation } from 'react-i18next'
-import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
-import { ToggleButton } from '@/primitives'
+import { RiGroupLine } from '@remixicon/react'
+import { ToggleButton, type ToggleButtonProps } from '@/primitives/ToggleButton'
import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
import { css } from '@/styled-system/css'
-import { useRemoteParticipants } from '@livekit/components-react'
-import { ToggleButtonProps } from '@/primitives/ToggleButton'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
-import { RoomEvent } from 'livekit-client'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
+import { ParticipantsCount } from '@/features/participants/components/ParticipantsCount'
+
+const containerStyles = css({ position: 'relative', display: 'inline-block' })
export const ParticipantsToggle = ({
onPress,
...props
}: ToggleButtonProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.participants' })
-
- /**
- * Context could not be used due to inconsistent refresh behavior.
- * The 'numParticipant' property on the room only updates when the room's metadata changes,
- * resulting in a delay compared to the participant list's actual refresh rate.
- */
- const remoteParticipants = useRemoteParticipants({
- updateOnlyOn: [
- RoomEvent.ParticipantConnected,
- RoomEvent.ParticipantDisconnected,
- ],
- })
- const numParticipants = remoteParticipants?.length + 1 // for the local participant
- const announcedCount =
- numParticipants && numParticipants > 0 ? numParticipants : 1
-
const { isParticipantsOpen, toggleParticipants } = useSidePanel()
+ const countId = useId()
const tooltipLabel = isParticipantsOpen ? 'open' : 'closed'
@@ -39,58 +25,32 @@ export const ParticipantsToggle = ({
handler: toggleParticipants,
})
+ const handlePress = useCallback(
+ (e: Parameters>[0]) => {
+ toggleParticipants()
+ onPress?.(e)
+ },
+ [toggleParticipants, onPress]
+ )
+
return (
-
+
{
- toggleParticipants()
- onPress?.(e)
- }}
+ onPress={handlePress}
data-attr={`controls-participants-${tooltipLabel}`}
{...props}
>
-
- {numParticipants < 100 ? (
- numParticipants || 1
- ) : (
-
- )}
-
+
)
}