mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-11 23:52:00 +00:00
🐛(frontend) honour Keep hand raised when picture-in-picture is open
The hand button owns the lower-hand timer and the toast, and the picture-in-picture window draws a second copy of that button, so two offers go up and dismissing one leaves the other to lower the hand. Move the watching into a component rendered once beside the room.
This commit is contained in:
@@ -34,6 +34,7 @@ and this project adheres to
|
||||
- 🐛(frontend) hide tooltips until they have a computed placement
|
||||
- 🐛(brevo) use django-lasuite for marketing management
|
||||
- ♿️(frontend) expose loading state to assistive technology
|
||||
- 🐛(frontend) honour Keep hand raised when picture-in-picture is open
|
||||
|
||||
## [1.33.0] - 2026-09-30
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useIsSpeaking, useRoomContext } from '@livekit/components-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||
import {
|
||||
closeLowerHandToasts,
|
||||
showLowerHandToast,
|
||||
} from '@/features/notifications/utils'
|
||||
|
||||
const SPEAKING_DETECTION_DELAY = 3000
|
||||
|
||||
/**
|
||||
* Offers to lower the local participant's raised hand after
|
||||
* SPEAKING_DETECTION_DELAY of speaking. Mount it once: each copy runs its own
|
||||
* timer and shows its own toast.
|
||||
*/
|
||||
export const LowerHandOnSpeaking = () => {
|
||||
const room = useRoomContext()
|
||||
const { isHandRaised, lowerHand } = useRaisedHand({
|
||||
participant: room.localParticipant,
|
||||
})
|
||||
const isSpeaking = useIsSpeaking(room.localParticipant)
|
||||
const [hasOffered, setHasOffered] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isHandRaised) return
|
||||
setHasOffered(false)
|
||||
closeLowerHandToasts()
|
||||
}, [isHandRaised])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSpeaking || !isHandRaised || hasOffered) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setHasOffered(true)
|
||||
showLowerHandToast(room.localParticipant, lowerHand)
|
||||
}, SPEAKING_DETECTION_DELAY)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSpeaking, isHandRaised, hasOffered])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -2,19 +2,12 @@ import { useTranslation } from 'react-i18next'
|
||||
import { RiHand } from '@remixicon/react'
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useIsSpeaking, useRoomContext } from '@livekit/components-react'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
closeLowerHandToasts,
|
||||
showLowerHandToast,
|
||||
} from '@/features/notifications/utils'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
const SPEAKING_DETECTION_DELAY = 3000
|
||||
|
||||
type Props = Pick<NonNullable<ButtonRecipeProps>, 'variant'> & ToggleButtonProps
|
||||
|
||||
export const HandToggle = ({
|
||||
@@ -25,64 +18,15 @@ export const HandToggle = ({
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
||||
|
||||
const room = useRoomContext()
|
||||
const { isHandRaised, toggleRaisedHand, lowerHand } = useRaisedHand({
|
||||
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||
participant: room.localParticipant,
|
||||
})
|
||||
|
||||
const isSpeaking = useIsSpeaking(room.localParticipant)
|
||||
const speakingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [hasShownToast, setHasShownToast] = useState(false)
|
||||
|
||||
const resetToastState = () => {
|
||||
setHasShownToast(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isHandRaised) return
|
||||
closeLowerHandToasts()
|
||||
}, [isHandRaised])
|
||||
|
||||
const handleToggle = () => {
|
||||
toggleRaisedHand()
|
||||
resetToastState()
|
||||
}
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'raise-hand',
|
||||
handler: handleToggle,
|
||||
handler: toggleRaisedHand,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShowToast = isSpeaking && isHandRaised && !hasShownToast
|
||||
|
||||
if (shouldShowToast && !speakingTimerRef.current) {
|
||||
speakingTimerRef.current = setTimeout(() => {
|
||||
speakingTimerRef.current = null
|
||||
setHasShownToast(true)
|
||||
const onClose = () => {
|
||||
lowerHand()
|
||||
resetToastState()
|
||||
}
|
||||
showLowerHandToast(room.localParticipant, onClose)
|
||||
}, SPEAKING_DETECTION_DELAY)
|
||||
}
|
||||
if ((!isSpeaking || !isHandRaised) && speakingTimerRef.current) {
|
||||
clearTimeout(speakingTimerRef.current)
|
||||
speakingTimerRef.current = null
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSpeaking, isHandRaised, hasShownToast, lowerHand])
|
||||
|
||||
// Clear any pending timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (speakingTimerRef.current) {
|
||||
clearTimeout(speakingTimerRef.current)
|
||||
speakingTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
||||
|
||||
return (
|
||||
@@ -100,7 +44,7 @@ export const HandToggle = ({
|
||||
tooltip={t(tooltipLabel)}
|
||||
isSelected={isHandRaised}
|
||||
onPress={(e) => {
|
||||
handleToggle()
|
||||
toggleRaisedHand()
|
||||
onPress?.(e)
|
||||
}}
|
||||
data-attr={`controls-hand-${tooltipLabel}`}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer'
|
||||
import { ChatProvider } from '@/features/chat/components/ChatProvider'
|
||||
import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences'
|
||||
import { RoomSilentMicDetector } from '@/features/rooms/components/SilentMicDetector'
|
||||
import { LowerHandOnSpeaking } from '@/features/rooms/livekit/components/LowerHandOnSpeaking'
|
||||
import { LobbyProvider } from '@/features/rooms/components/LobbyProvider'
|
||||
|
||||
/**
|
||||
@@ -119,6 +120,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
<ConnectionObserver />
|
||||
<SyncDevicePreferences />
|
||||
<RoomSilentMicDetector />
|
||||
<LowerHandOnSpeaking />
|
||||
<MediaStateObserver />
|
||||
<ChatProvider />
|
||||
<LobbyProvider />
|
||||
|
||||
Reference in New Issue
Block a user