Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise hand V1 #146

Merged
merged 10 commits into from
Sep 3, 2024
6 changes: 6 additions & 0 deletions src/frontend/panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ const config: Config = {
'50%': { height: '4px' },
'100%': { height: '8px' },
},
wave_hand: {
'0%': { transform: 'rotate(0deg)' },
'20%': { transform: 'rotate(-20deg)' },
'80%': { transform: 'rotate(20deg)' },
'100%': { transform: 'rotate(0)' },
},
},
tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData'

export const useLowerHandParticipant = () => {
const data = useRoomData()

const lowerHandParticipant = (participant: Participant) => {
if (!data || !data?.livekit) {
throw new Error('Room data is not available')
}
const newMetadata = JSON.parse(participant.metadata || '{}')
newMetadata.raised = !newMetadata.raised
return fetchServerApi(
buildServerApiUrl(
data.livekit.url,
'twirp/livekit.RoomService/UpdateParticipant'
),
data.livekit.token,
{
method: 'POST',
body: JSON.stringify({
room: data.livekit.room,
identity: participant.identity,
metadata: JSON.stringify(newMetadata),
permission: participant.permissions,
}),
}
)
}
return { lowerHandParticipant }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Participant } from 'livekit-client'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant'

export const useLowerHandParticipants = () => {
const { lowerHandParticipant } = useLowerHandParticipant()

const lowerHandParticipants = (participants: Array<Participant>) => {
try {
const promises = participants.map((participant) =>
lowerHandParticipant(participant)
)
return Promise.all(promises)
} catch (error) {
throw new Error('An error occurred while lowering hands.')
}
}
return { lowerHandParticipants }
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
} 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'

export function TrackRefContextIfNeeded(
props: React.PropsWithChildren<{
Expand Down Expand Up @@ -84,6 +86,10 @@ export const ParticipantTile: (
[trackReference, layoutContext]
)

const { isHandRaised } = useRaisedHand({
participant: trackReference.participant,
})

return (
<div ref={ref} style={{ position: 'relative' }} {...elementProps}>
<TrackRefContextIfNeeded trackRef={trackReference}>
Expand Down Expand Up @@ -113,9 +119,26 @@ export const ParticipantTile: (
/>
</div>
<div className="lk-participant-metadata">
<div className="lk-participant-metadata-item">
<div
className="lk-participant-metadata-item"
style={{
minHeight: '24px',
}}
>
{trackReference.source === Track.Source.Camera ? (
<>
{isHandRaised && (
<RiHand
color="white"
size={16}
style={{
marginInlineEnd: '.25rem', // fixme - match TrackMutedIndicator styling
animationDuration: '300ms',
animationName: 'wave_hand',
animationIterationCount: '2',
}}
/>
)}
{isEncrypted && (
<LockLockedIcon style={{ marginRight: '0.25rem' }} />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useTranslation } from 'react-i18next'
import { RiHand } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { css } from '@/styled-system/css'
import { useLocalParticipant } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'

export const HandToggle = () => {
const { t } = useTranslation('rooms')

const localParticipant = useLocalParticipant().localParticipant
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: localParticipant,
})

const label = isHandRaised
? t('controls.hand.lower')
: t('controls.hand.raise')

return (
<div
className={css({
position: 'relative',
display: 'inline-block',
})}
>
<ToggleButton
square
legacyStyle
aria-label={label}
tooltip={label}
isSelected={isHandRaised}
onPress={() => toggleRaisedHand()}
>
<RiHand />
</ToggleButton>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { css } from '@/styled-system/css'

import { HStack } from '@/styled-system/jsx'
import { Text } from '@/primitives/Text'
import { useTranslation } from 'react-i18next'
import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { RiHand } from '@remixicon/react'
import { ListItemActionButton } from '@/features/rooms/livekit/components/controls/Participants/ListItemActionButton'
import { useLowerHandParticipant } from '@/features/rooms/livekit/api/lowerHandParticipant.ts'

type HandRaisedListItemProps = {
participant: Participant
}

export const HandRaisedListItem = ({
participant,
}: HandRaisedListItemProps) => {
const { t } = useTranslation('rooms')
const name = participant.name || participant.identity

const { lowerHandParticipant } = useLowerHandParticipant()

return (
<HStack
role="listitem"
justify="space-between"
key={participant.identity}
id={participant.identity}
className={css({
padding: '0.25rem 0',
width: 'full',
})}
>
<HStack>
<Avatar name={name} bgColor={getParticipantColor(participant)} />
<Text
variant={'sm'}
className={css({
userSelect: 'none',
cursor: 'default',
display: 'flex',
})}
>
<span
className={css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '120px',
display: 'block',
})}
>
{name}
</span>
{isLocal(participant) && (
<span
className={css({
marginLeft: '.25rem',
whiteSpace: 'nowrap',
})}
>
({t('participants.you')})
</span>
)}
</Text>
</HStack>
<ListItemActionButton
onPress={() => lowerHandParticipant(participant)}
tooltip={t('participants.lowerParticipantHand', { name })}
>
<RiHand color="gray" />
</ListItemActionButton>
</HStack>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { styled } from '@/styled-system/jsx'
import {
Button as RACButton,
ButtonProps as RACButtonsProps,
} from 'react-aria-components'
import {
TooltipWrapper,
TooltipWrapperProps,
} from '@/primitives/TooltipWrapper'

const StyledButton = styled(RACButton, {
base: {
padding: '10px',
minWidth: '24px',
minHeight: '24px',
borderRadius: '50%',
backgroundColor: 'transparent',
transition: 'background 200ms',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
'&[data-focused]': {
backgroundColor: '#f5f5f5',
},
},
})

export const ListItemActionButton = ({
tooltip,
tooltipType = 'instant',
children,
...props
}: RACButtonsProps & TooltipWrapperProps) => {
return (
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
<StyledButton {...props}>{children}</StyledButton>
</TooltipWrapper>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Button } from '@/primitives'
import { useLowerHandParticipants } from '@/features/rooms/livekit/api/lowerHandParticipants'
import { useTranslation } from 'react-i18next'
import { Participant } from 'livekit-client'

type LowerAllHandsButtonProps = {
participants: Array<Participant>
}

export const LowerAllHandsButton = ({
participants,
}: LowerAllHandsButtonProps) => {
const { lowerHandParticipants } = useLowerHandParticipants()
const { t } = useTranslation('rooms')
return (
<Button
aria-label={t('participants.lowerParticipantsHand')}
size="sm"
fullWidth
invisible
onPress={() => lowerHandParticipants(participants)}
>
{t('participants.lowerParticipantsHand')}
</Button>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@ import { Avatar } from '@/components/Avatar'
import { getParticipantColor } from '@/features/rooms/utils/getParticipantColor'
import { Participant, Track } from 'livekit-client'
import { isLocal } from '@/utils/livekit'
import { Button as RACButton } from 'react-aria-components'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import {
useIsSpeaking,
useTrackMutedIndicator,
} from '@livekit/components-react'
import Source = Track.Source
import { RiMicOffLine } from '@remixicon/react'
import { TooltipWrapper } from '@/primitives/TooltipWrapper.tsx'
import { Button, Dialog, P } from '@/primitives'
import { useState } from 'react'
import { useMuteParticipant } from '@/features/rooms/livekit/api/muteParticipant'
import { ListItemActionButton } from '@/features/rooms/livekit/components/controls/Participants/ListItemActionButton'

const MuteAlertDialog = ({
isOpen,
Expand Down Expand Up @@ -67,37 +66,19 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {

return (
<>
<TooltipWrapper
<ListItemActionButton
tooltip={t('participants.muteParticipant', {
name,
})}
tooltipType="instant"
isDisabled={isDisabled}
onPress={() => !isMuted && setIsAlertOpen(true)}
>
<RACButton
isDisabled={isDisabled}
className={css({
padding: '10px',
minWidth: '24px',
minHeight: '24px',
borderRadius: '50%',
backgroundColor: 'transparent',
transition: 'background 200ms',
'&[data-hovered]': {
backgroundColor: '#f5f5f5',
},
'&[data-focused]': {
backgroundColor: '#f5f5f5',
},
})}
onPress={() => !isMuted && setIsAlertOpen(true)}
>
{isMuted ? (
<RiMicOffLine color="gray" />
) : (
<ActiveSpeaker isSpeaking={isSpeaking} />
)}
</RACButton>
</TooltipWrapper>
{isMuted ? (
<RiMicOffLine color="gray" />
) : (
<ActiveSpeaker isSpeaking={isSpeaking} />
)}
</ListItemActionButton>
<MuteAlertDialog
isOpen={isAlertOpen}
onSubmit={() =>
Expand Down
Loading