✨(frontend) notify user when hands are raised
Using toast, first draft of a toast notification inspired by Gmeet. Users can open the waiting list to see all the raised hands.
This commit is contained in:
committed by
aleb_the_flash
parent
e21858febe
commit
a402c2f46f
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRoomContext } from '@livekit/components-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 { ToastProvider, toastQueue } from './components/ToastProvider'
|
||||||
import { NotificationType } from './NotificationType'
|
import { NotificationType } from './NotificationType'
|
||||||
import { Div } from '@/primitives'
|
import { Div } from '@/primitives'
|
||||||
@@ -27,6 +27,45 @@ export const MainNotificationToast = () => {
|
|||||||
}
|
}
|
||||||
}, [room])
|
}, [room])
|
||||||
|
|
||||||
|
// fixme - close all related toasters when hands are lowered remotely
|
||||||
|
useEffect(() => {
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
|
||||||
|
const handleNotificationReceived = (
|
||||||
|
payload: Uint8Array,
|
||||||
|
participant?: RemoteParticipant
|
||||||
|
) => {
|
||||||
|
if (!participant) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const notification = decoder.decode(payload)
|
||||||
|
const existingToast = toastQueue.visibleToasts.find(
|
||||||
|
(toast) =>
|
||||||
|
toast.content.participant === participant &&
|
||||||
|
toast.content.type === NotificationType.Raised
|
||||||
|
)
|
||||||
|
if (existingToast && notification === NotificationType.Lowered) {
|
||||||
|
toastQueue.close(existingToast.key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!existingToast && notification === NotificationType.Raised) {
|
||||||
|
toastQueue.add(
|
||||||
|
{
|
||||||
|
participant,
|
||||||
|
type: NotificationType.Raised,
|
||||||
|
},
|
||||||
|
{ timeout: 5000 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
room.on(RoomEvent.DataReceived, handleNotificationReceived)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
room.off(RoomEvent.DataReceived, handleNotificationReceived)
|
||||||
|
}
|
||||||
|
}, [room])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
|
<Div position="absolute" bottom={20} right={5} zIndex={1000}>
|
||||||
<ToastProvider />
|
<ToastProvider />
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export enum NotificationType {
|
export enum NotificationType {
|
||||||
Joined = 'joined',
|
Joined = 'joined',
|
||||||
Default = 'default',
|
Default = 'default',
|
||||||
|
Raised = 'raised',
|
||||||
|
Lowered = 'lowered',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useToast } from '@react-aria/toast'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
import { StyledToastContainer, ToastProps } from './Toast'
|
||||||
|
import { HStack } from '@/styled-system/jsx'
|
||||||
|
import { Button, Div } from '@/primitives'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiCloseLine, RiHand } from '@remixicon/react'
|
||||||
|
import { useWidgetInteraction } from '@/features/rooms/livekit/hooks/useWidgetInteraction'
|
||||||
|
|
||||||
|
export function ToastRaised({ state, ...props }: ToastProps) {
|
||||||
|
const { t } = useTranslation('notifications')
|
||||||
|
const ref = useRef(null)
|
||||||
|
const { toastProps, contentProps, titleProps, closeButtonProps } = useToast(
|
||||||
|
props,
|
||||||
|
state,
|
||||||
|
ref
|
||||||
|
)
|
||||||
|
const participant = props.toast.content.participant
|
||||||
|
const { isParticipantsOpen, toggleParticipants } = useWidgetInteraction()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledToastContainer {...toastProps} ref={ref}>
|
||||||
|
<HStack
|
||||||
|
justify="center"
|
||||||
|
alignItems="center"
|
||||||
|
{...contentProps}
|
||||||
|
padding={14}
|
||||||
|
gap={0}
|
||||||
|
>
|
||||||
|
<RiHand
|
||||||
|
color="white"
|
||||||
|
style={{
|
||||||
|
marginRight: '1rem',
|
||||||
|
animationDuration: '300ms',
|
||||||
|
animationName: 'wave_hand',
|
||||||
|
animationIterationCount: '2',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Div {...titleProps} marginRight={0.5}>
|
||||||
|
{t('raised.description', {
|
||||||
|
name: participant.name || t('defaultName'),
|
||||||
|
})}
|
||||||
|
</Div>
|
||||||
|
{!isParticipantsOpen && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="text"
|
||||||
|
style={{
|
||||||
|
color: '#60a5fa',
|
||||||
|
}}
|
||||||
|
onPress={(e) => {
|
||||||
|
toggleParticipants()
|
||||||
|
closeButtonProps.onPress?.(e)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('raised.cta')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button square size="sm" invisible {...closeButtonProps}>
|
||||||
|
<RiCloseLine size={18} color="white" />
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</StyledToastContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { useRef } from 'react'
|
|||||||
import { NotificationType } from '../NotificationType'
|
import { NotificationType } from '../NotificationType'
|
||||||
import { ToastJoined } from './ToastJoined'
|
import { ToastJoined } from './ToastJoined'
|
||||||
import { ToastData } from './ToastProvider'
|
import { ToastData } from './ToastProvider'
|
||||||
|
import { ToastRaised } from '@/features/notifications/components/ToastRaised.tsx'
|
||||||
|
|
||||||
interface ToastRegionProps extends AriaToastRegionProps {
|
interface ToastRegionProps extends AriaToastRegionProps {
|
||||||
state: ToastState<ToastData>
|
state: ToastState<ToastData>
|
||||||
@@ -19,6 +20,9 @@ export function ToastRegion({ state, ...props }: ToastRegionProps) {
|
|||||||
if (toast.content?.type === NotificationType.Joined) {
|
if (toast.content?.type === NotificationType.Joined) {
|
||||||
return <ToastJoined key={toast.key} toast={toast} state={state} />
|
return <ToastJoined key={toast.key} toast={toast} state={state} />
|
||||||
}
|
}
|
||||||
|
if (toast.content?.type === NotificationType.Raised) {
|
||||||
|
return <ToastRaised 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>
|
||||||
|
|||||||
@@ -2,21 +2,33 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { RiHand } from '@remixicon/react'
|
import { RiHand } from '@remixicon/react'
|
||||||
import { ToggleButton } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useLocalParticipant } 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 { NotificationType } from '@/features/notifications/NotificationType'
|
||||||
|
|
||||||
export const HandToggle = () => {
|
export const HandToggle = () => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
|
|
||||||
const localParticipant = useLocalParticipant().localParticipant
|
const room = useRoomContext()
|
||||||
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||||
participant: localParticipant,
|
participant: room.localParticipant,
|
||||||
})
|
})
|
||||||
|
|
||||||
const label = isHandRaised
|
const label = isHandRaised
|
||||||
? t('controls.hand.lower')
|
? t('controls.hand.lower')
|
||||||
: t('controls.hand.raise')
|
: t('controls.hand.raise')
|
||||||
|
|
||||||
|
const notifyOtherParticipants = (isHandRaised: boolean) => {
|
||||||
|
room.localParticipant.publishData(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
!isHandRaised ? NotificationType.Raised : NotificationType.Lowered
|
||||||
|
),
|
||||||
|
{
|
||||||
|
reliable: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
@@ -30,7 +42,10 @@ export const HandToggle = () => {
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
tooltip={label}
|
tooltip={label}
|
||||||
isSelected={isHandRaised}
|
isSelected={isHandRaised}
|
||||||
onPress={() => toggleRaisedHand()}
|
onPress={() => {
|
||||||
|
notifyOtherParticipants(isHandRaised)
|
||||||
|
toggleRaisedHand()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<RiHand />
|
<RiHand />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
"defaultName": "",
|
"defaultName": "",
|
||||||
"joined": {
|
"joined": {
|
||||||
"description": ""
|
"description": ""
|
||||||
|
},
|
||||||
|
"raised": {
|
||||||
|
"description": "",
|
||||||
|
"cta": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
"defaultName": "A contributor",
|
"defaultName": "A contributor",
|
||||||
"joined": {
|
"joined": {
|
||||||
"description": "{{name}} join the room"
|
"description": "{{name}} join the room"
|
||||||
|
},
|
||||||
|
"raised": {
|
||||||
|
"description": "{{name}} raised its hand.",
|
||||||
|
"cta": "Open waiting list"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
"defaultName": "Un contributeur",
|
"defaultName": "Un contributeur",
|
||||||
"joined": {
|
"joined": {
|
||||||
"description": "{{name}} participe à l'appel"
|
"description": "{{name}} participe à l'appel"
|
||||||
|
},
|
||||||
|
"raised": {
|
||||||
|
"description": "{{name}} a levé la main.",
|
||||||
|
"cta": "Ouvrir la file d'attente"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const buttonRecipe = cva({
|
|||||||
color: 'primary',
|
color: 'primary',
|
||||||
'&[data-hovered]': {
|
'&[data-hovered]': {
|
||||||
background: 'gray.100 !important',
|
background: 'gray.100 !important',
|
||||||
|
color: 'primary !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
|
|||||||
Reference in New Issue
Block a user