✨(frontend) lower hand autonomously for active speakers
Users requested that their raised hands be lowered automatically if they become the active speaker. This change ensures a more natural experience during discussions, preventing user from forgetting to lower their hand while speaking.
This commit is contained in:
committed by
aleb_the_flash
parent
13bb6acdf6
commit
291544bd52
@@ -3,4 +3,5 @@ export enum NotificationType {
|
|||||||
HandRaised = 'handRaised',
|
HandRaised = 'handRaised',
|
||||||
ParticipantMuted = 'participantMuted',
|
ParticipantMuted = 'participantMuted',
|
||||||
MessageReceived = 'messageReceived',
|
MessageReceived = 'messageReceived',
|
||||||
|
LowerHand = 'lowerHand',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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'
|
||||||
|
import { Button } from '@/primitives'
|
||||||
|
|
||||||
|
export function ToastLowerHand({ state, ...props }: ToastProps) {
|
||||||
|
const { t } = useTranslation('notifications', { keyPrefix: 'lowerHand' })
|
||||||
|
const ref = useRef(null)
|
||||||
|
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||||
|
const toast = props.toast
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
// Clear onClose handler to prevent lowering the hand when user dismisses
|
||||||
|
toast.onClose = undefined
|
||||||
|
state.close(toast.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledToastContainer {...toastProps} ref={ref}>
|
||||||
|
<HStack
|
||||||
|
justify="center"
|
||||||
|
alignItems="center"
|
||||||
|
{...contentProps}
|
||||||
|
padding={14}
|
||||||
|
gap={0}
|
||||||
|
>
|
||||||
|
<p>{t('auto')}</p>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="text"
|
||||||
|
style={{
|
||||||
|
color: '#60a5fa',
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
}}
|
||||||
|
onPress={() => handleDismiss()}
|
||||||
|
>
|
||||||
|
{t('dismiss')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</StyledToastContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { ToastData } from './ToastProvider'
|
|||||||
import { ToastRaised } from './ToastRaised'
|
import { ToastRaised } from './ToastRaised'
|
||||||
import { ToastMuted } from './ToastMuted'
|
import { ToastMuted } from './ToastMuted'
|
||||||
import { ToastMessageReceived } from './ToastMessageReceived'
|
import { ToastMessageReceived } from './ToastMessageReceived'
|
||||||
|
import { ToastLowerHand } from './ToastLowerHand'
|
||||||
|
|
||||||
interface ToastRegionProps extends AriaToastRegionProps {
|
interface ToastRegionProps extends AriaToastRegionProps {
|
||||||
state: ToastState<ToastData>
|
state: ToastState<ToastData>
|
||||||
@@ -33,6 +34,9 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) {
|
|||||||
<ToastMessageReceived key={toast.key} toast={toast} state={state} />
|
<ToastMessageReceived key={toast.key} toast={toast} state={state} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (toast.content?.type === NotificationType.LowerHand) {
|
||||||
|
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
|
||||||
|
}
|
||||||
return <Toast key={toast.key} toast={toast} state={state} />
|
return <Toast key={toast.key} toast={toast} state={state} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
28
src/frontend/src/features/notifications/utils.ts
Normal file
28
src/frontend/src/features/notifications/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { toastQueue } from './components/ToastProvider'
|
||||||
|
import { NotificationType } from './NotificationType'
|
||||||
|
import { NotificationDuration } from './NotificationDuration'
|
||||||
|
import { Participant } from 'livekit-client'
|
||||||
|
|
||||||
|
export const showLowerHandToast = (
|
||||||
|
participant: Participant,
|
||||||
|
onClose: () => void
|
||||||
|
) => {
|
||||||
|
toastQueue.add(
|
||||||
|
{
|
||||||
|
participant,
|
||||||
|
type: NotificationType.LowerHand,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: NotificationDuration.LOWER_HAND,
|
||||||
|
onClose,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const closeLowerHandToasts = () => {
|
||||||
|
toastQueue.visibleToasts.forEach((toast) => {
|
||||||
|
if (toast.content.type === NotificationType.LowerHand) {
|
||||||
|
toastQueue.close(toast.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,6 +4,13 @@ import { ToggleButton } from '@/primitives'
|
|||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useRoomContext } from '@livekit/components-react'
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
closeLowerHandToasts,
|
||||||
|
showLowerHandToast,
|
||||||
|
} from '@/features/notifications/utils'
|
||||||
|
|
||||||
|
const SPEAKING_DETECTION_DELAY = 3000
|
||||||
|
|
||||||
export const HandToggle = () => {
|
export const HandToggle = () => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
||||||
@@ -13,6 +20,39 @@ export const HandToggle = () => {
|
|||||||
participant: room.localParticipant,
|
participant: room.localParticipant,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isSpeaking = room.localParticipant.isSpeaking
|
||||||
|
const speakingTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const [hasShownToast, setHasShownToast] = useState(false)
|
||||||
|
|
||||||
|
const resetToastState = () => {
|
||||||
|
setHasShownToast(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isHandRaised) return
|
||||||
|
closeLowerHandToasts()
|
||||||
|
}, [isHandRaised])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldShowToast = isSpeaking && isHandRaised && !hasShownToast
|
||||||
|
|
||||||
|
if (shouldShowToast && !speakingTimerRef.current) {
|
||||||
|
speakingTimerRef.current = setTimeout(() => {
|
||||||
|
setHasShownToast(true)
|
||||||
|
const onClose = () => {
|
||||||
|
if (isHandRaised) toggleRaisedHand()
|
||||||
|
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, toggleRaisedHand])
|
||||||
|
|
||||||
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +68,10 @@ export const HandToggle = () => {
|
|||||||
aria-label={t(tooltipLabel)}
|
aria-label={t(tooltipLabel)}
|
||||||
tooltip={t(tooltipLabel)}
|
tooltip={t(tooltipLabel)}
|
||||||
isSelected={isHandRaised}
|
isSelected={isHandRaised}
|
||||||
onPress={() => toggleRaisedHand()}
|
onPress={() => {
|
||||||
|
toggleRaisedHand()
|
||||||
|
resetToastState()
|
||||||
|
}}
|
||||||
data-attr={`controls-hand-${tooltipLabel}`}
|
data-attr={`controls-hand-${tooltipLabel}`}
|
||||||
>
|
>
|
||||||
<RiHand />
|
<RiHand />
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
"cta": ""
|
"cta": ""
|
||||||
},
|
},
|
||||||
"muted": "",
|
"muted": "",
|
||||||
"openChat": ""
|
"openChat": "",
|
||||||
|
"lowerHand": {
|
||||||
|
"auto": "",
|
||||||
|
"dismiss": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
"cta": "Open waiting list"
|
"cta": "Open waiting list"
|
||||||
},
|
},
|
||||||
"muted": "{{name}} has muted your microphone. No participant can hear you.",
|
"muted": "{{name}} has muted your microphone. No participant can hear you.",
|
||||||
"openChat": "Open chat"
|
"openChat": "Open chat",
|
||||||
|
"lowerHand": {
|
||||||
|
"auto": "It seems you have started speaking, so your hand will be lowered.",
|
||||||
|
"dismiss": "Keep hand raised"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
"cta": "Ouvrir la file d'attente"
|
"cta": "Ouvrir la file d'attente"
|
||||||
},
|
},
|
||||||
"muted": "{{name}} a coupé votre micro. Aucun participant ne peut l'entendre.",
|
"muted": "{{name}} a coupé votre micro. Aucun participant ne peut l'entendre.",
|
||||||
"openChat": "Ouvrir le chat"
|
"openChat": "Ouvrir le chat",
|
||||||
|
"lowerHand": {
|
||||||
|
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
|
||||||
|
"dismiss": "Laisser la main levée"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user