From 6a093480799e6364b26c5e6c9c8ec6733aea8c02 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 2 Sep 2024 15:12:18 +0200 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8(frontend)=20introduce=20raise?= =?UTF-8?q?=20hand=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspired by Magnify. Rely on participant metadata to determine, if the local user has raised its hand. There is a todo item in LiveKit code, useLocalParticipant is not subscribed to metadata updates events. The contrast of the toggled button in legacy Style is poor, it needs to be urgently improved. User can barely distinguish if their hands are raised. --- .../components/controls/HandToggle.tsx | 46 +++++++++++++++++++ .../rooms/livekit/prefabs/ControlBar.tsx | 2 + src/frontend/src/locales/de/rooms.json | 4 ++ src/frontend/src/locales/en/rooms.json | 4 ++ src/frontend/src/locales/fr/rooms.json | 4 ++ 5 files changed, 60 insertions(+) create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx diff --git a/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx new file mode 100644 index 00000000..92bd3cb5 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx @@ -0,0 +1,46 @@ +import { useTranslation } from 'react-i18next' +import { RiHand } from '@remixicon/react' +import { ToggleButton } from '@/primitives' +import { css } from '@/styled-system/css' +import { + useLocalParticipant, + useParticipantInfo, +} from '@livekit/components-react' + +export const HandToggle = () => { + const { t } = useTranslation('rooms') + + const localParticipant = useLocalParticipant().localParticipant + const { metadata } = useParticipantInfo({ participant: localParticipant }) + + const parsedMetadata = JSON.parse(metadata || '{}') + + const sendRaise = () => { + parsedMetadata.raised = !parsedMetadata.raised + localParticipant.setMetadata(JSON.stringify(parsedMetadata)) + } + + const label = parsedMetadata.raised + ? t('controls.hand.lower') + : t('controls.hand.raise') + + return ( +
+ sendRaise()} + > + + +
+ ) +} diff --git a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx index 6361eef8..6fe0b89d 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/ControlBar.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next' import { OptionsButton } from '../components/controls/Options/OptionsButton' import { ParticipantsToggle } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsToggle' import { ChatToggle } from '@/features/rooms/livekit/components/controls/ChatToggle' +import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle' /** @public */ export type ControlBarControls = { @@ -183,6 +184,7 @@ export function ControlBar({ )} )} + diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 7e6a3b1b..40a49cfc 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -32,6 +32,10 @@ "open": "", "closed": "" }, + "hand": { + "raise": "", + "lower": "" + }, "leave": "", "participants": { "open": "", diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 42f44adb..ab668143 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -32,6 +32,10 @@ "open": "Close the chat", "closed": "Open the chat" }, + "hand": { + "raise": "Raise hand", + "lower": "Lower hand" + }, "leave": "Leave", "participants": { "open": "Hide everyone", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 14a44e67..0e719b68 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -32,6 +32,10 @@ "open": "Masquer le chat", "closed": "Afficher le chat" }, + "hand": { + "raise": "Lever la main", + "lower": "Baisser la main" + }, "leave": "Quitter", "participants": { "open": "Masquer les participants", From 24de5d7e8488d134cb713ae6f0db004ea6b455f0 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 2 Sep 2024 18:14:42 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20introduce=20sect?= =?UTF-8?q?ions=20in=20participants=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why? This layout is extensible. This menu will have two sections, to separate in-room participants from the waiting room ones. It's copied from Gmeet User Experience. In-room participants will be divided in sub-lists based on their states (ex: hand raised, …). This User Experience is extensible if in the future with support subroom in a room or whatever. --- .../ParticipantsCollapsableList.tsx | 106 ++++++++++++++++++ .../Participants/ParticipantsList.tsx | 66 +++++------ src/frontend/src/locales/de/rooms.json | 6 + src/frontend/src/locales/en/rooms.json | 6 + src/frontend/src/locales/fr/rooms.json | 6 + 5 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx new file mode 100644 index 00000000..66373772 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react' +import { css } from '@/styled-system/css' +import { ToggleButton } from 'react-aria-components' +import { HStack, styled, VStack } from '@/styled-system/jsx' +import { RiArrowUpSLine } from '@remixicon/react' +import { Participant } from 'livekit-client' +import { useTranslation } from 'react-i18next' + +const ToggleHeader = styled(ToggleButton, { + base: { + minHeight: '40px', //fixme hardcoded value + paddingRight: '.5rem', + cursor: 'pointer', + display: 'flex', + justifyContent: 'space-between', + width: '100%', + alignItems: 'center', + transition: 'background 200ms', + borderTopRadius: '7px', + '&[data-hovered]': { + backgroundColor: '#f5f5f5', + }, + }, +}) + +const Container = styled('div', { + base: { + border: '1px solid #dadce0', + borderRadius: '8px', + margin: '0 .625rem', + }, +}) + +const ListContainer = styled(VStack, { + base: { + borderTop: '1px solid #dadce0', + alignItems: 'start', + overflowY: 'scroll', + overflowX: 'hidden', + minHeight: 0, + flexGrow: 1, + display: 'flex', + paddingY: '0.5rem', + paddingX: '1rem', + gap: 0, + }, +}) + +type ParticipantsCollapsableListProps = { + heading: string + participants: Array + renderParticipant: (participant: Participant) => JSX.Element +} + +export const ParticipantsCollapsableList = ({ + heading, + participants, + renderParticipant, +}: ParticipantsCollapsableListProps) => { + const { t } = useTranslation('rooms') + const [isOpen, setIsOpen] = useState(true) + const label = t(`participants.collapsable.${isOpen ? 'close' : 'open'}`, { + name: heading, + }) + return ( + + setIsOpen(!isOpen)} + style={{ + borderRadius: !isOpen ? '7px' : undefined, + }} + > + +
+ {heading} +
+
{participants?.length || 0}
+
+ +
+ {isOpen && ( + + {participants.map((participant) => renderParticipant(participant))} + + )} +
+ ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index dd82ba5c..cbcba0c4 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -2,14 +2,14 @@ import { css } from '@/styled-system/css' import { useParticipants } from '@livekit/components-react' import { Heading } from 'react-aria-components' -import { Box, Button, Div } from '@/primitives' +import { Box, Button, Div, H } from '@/primitives' import { text } from '@/primitives/Text' import { RiCloseLine } from '@remixicon/react' import { participantsStore } from '@/stores/participants' import { useTranslation } from 'react-i18next' import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events' import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem' -import { VStack } from '@/styled-system/jsx' +import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList' // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short. export const ParticipantsList = () => { @@ -39,25 +39,26 @@ export const ParticipantsList = () => { return ( - - {t('participants.heading')}{' '} - - {participants?.length} - + + {t('participants.heading')}
- {sortedParticipants?.length > 0 && ( - - {sortedParticipants.map((participant) => ( - - ))} - - )} + + {t('participants.subheading').toUpperCase()} + + ( + + )} + />
) } diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 40a49cfc..da63ccd1 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -53,7 +53,13 @@ }, "participants": { "heading": "", + "subheading": "", "closeButton": "", + "contributors": "", + "collapsable": { + "open": "", + "close": "" + }, "you": "", "muteParticipant": "", "muteParticipantAlert": { diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index ab668143..b31a7938 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -53,8 +53,14 @@ }, "participants": { "heading": "Participants", + "subheading": "In room", "closeButton": "Hide participants", "you": "You", + "contributors": "Contributors", + "collapsable": { + "open": "Open {{name}} list", + "close": "Close {{name}} list" + }, "muteParticipant": "Close the mic of {{name}}", "muteParticipantAlert": { "description": "Mute {{name}} for all participants? {{name}} is the only person who can unmute themselves.", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 0e719b68..64186e8a 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -53,8 +53,14 @@ }, "participants": { "heading": "Participants", + "subheading": "Dans la réunion", "closeButton": "Masquer les participants", "you": "Vous", + "contributors": "Contributeurs", + "collapsable": { + "open": "Ouvrir la liste {{name}}", + "close": "Fermer la liste {{name}}" + }, "muteParticipant": "Couper le micro de {{name}}", "muteParticipantAlert": { "description": "Couper le micro de {{name}} pour tous les participants ? {{name}} est la seule personne habilitée à réactiver son micro", From bce05a6b5d18493703d025811d95f7592608c718 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 2 Sep 2024 19:06:10 +0200 Subject: [PATCH 03/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20extract=20?= =?UTF-8?q?action=20icon=20button=20in=20a=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Will need it for lower hand action. Actually, this piece of code should be enhanced. It'll act as a corner stone of all actions dispatched in the interface. I'll continue refactoring it later on. --- .../Participants/ListItemActionButton.tsx | 39 +++++++++++++++++++ .../Participants/ParticipantListItem.tsx | 39 +++++-------------- 2 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/Participants/ListItemActionButton.tsx diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ListItemActionButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ListItemActionButton.tsx new file mode 100644 index 00000000..aa885a42 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ListItemActionButton.tsx @@ -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 ( + + {children} + + ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx index f0f88fe8..3dfa7a82 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantListItem.tsx @@ -7,7 +7,6 @@ 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, @@ -15,10 +14,10 @@ import { } 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, @@ -67,37 +66,19 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => { return ( <> - !isMuted && setIsAlertOpen(true)} > - !isMuted && setIsAlertOpen(true)} - > - {isMuted ? ( - - ) : ( - - )} - - + {isMuted ? ( + + ) : ( + + )} + From 41755f83d09c41e900aa0f3723e765fb65798fe9 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 3 Sep 2024 09:59:21 +0200 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20raised=20hand?= =?UTF-8?q?s=20waiting=20room?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show the raised hand waiting list, allow any participant to lower other participants' hands. Inspired from Gmeet. I found it quite clear to have a dedicated waiting list for raised hands. --- .../rooms/livekit/api/lowerHandParticipant.ts | 33 ++++++++ .../Participants/HandRaisedListItem.tsx | 78 +++++++++++++++++++ .../Participants/ParticipantsList.tsx | 21 +++++ src/frontend/src/locales/de/rooms.json | 4 +- src/frontend/src/locales/en/rooms.json | 4 +- src/frontend/src/locales/fr/rooms.json | 4 +- 6 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx diff --git a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts new file mode 100644 index 00000000..e9d91fe4 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts @@ -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 } +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx new file mode 100644 index 00000000..0571d066 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/HandRaisedListItem.tsx @@ -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 ( + + + + + + {name} + + {isLocal(participant) && ( + + ({t('participants.you')}) + + )} + + + lowerHandParticipant(participant)} + tooltip={t('participants.lowerParticipantHand', { name })} + > + + + + ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index cbcba0c4..16ca082b 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next' import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/events' import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem' import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList' +import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem' // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short. export const ParticipantsList = () => { @@ -35,6 +36,11 @@ export const ParticipantsList = () => { ...sortedRemoteParticipants, ] + const raisedHandParticipants = participants.filter((participant) => { + const data = JSON.parse(participant.metadata || '{}') + return data.raised + }) + // TODO - extract inline styling in a centralized styling file, and avoid magic numbers return ( { > {t('participants.subheading').toUpperCase()} + {raisedHandParticipants.length > 0 && ( +
+ ( + + )} + /> +
+ )} Date: Tue, 3 Sep 2024 11:25:35 +0200 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8(frontend)=20allow=20lowering=20?= =?UTF-8?q?all=20hands=20at=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspired from Gmeet. Add a button to lower all hands at once. It should be quite useful for room moderator/admin. The UX might feel strange, having this action button at the top of the list, but I could not figure out a better layout. In terms of UX I feel this is the best we can provide. However, the UI should definitely be improved. --- .../livekit/api/lowerHandParticipants.ts | 18 +++++++++++++ .../Participants/LowerAllHandsButton.tsx | 26 +++++++++++++++++++ .../ParticipantsCollapsableList.tsx | 3 +++ .../Participants/ParticipantsList.tsx | 4 +++ src/frontend/src/locales/de/rooms.json | 3 ++- src/frontend/src/locales/en/rooms.json | 3 ++- src/frontend/src/locales/fr/rooms.json | 3 ++- 7 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/features/rooms/livekit/api/lowerHandParticipants.ts create mode 100644 src/frontend/src/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton.tsx diff --git a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipants.ts b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipants.ts new file mode 100644 index 00000000..2568b261 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipants.ts @@ -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) => { + 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 } +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton.tsx new file mode 100644 index 00000000..79a4ba1a --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton.tsx @@ -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 +} + +export const LowerAllHandsButton = ({ + participants, +}: LowerAllHandsButtonProps) => { + const { lowerHandParticipants } = useLowerHandParticipants() + const { t } = useTranslation('rooms') + return ( + + ) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx index 66373772..46699875 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList.tsx @@ -50,12 +50,14 @@ type ParticipantsCollapsableListProps = { heading: string participants: Array renderParticipant: (participant: Participant) => JSX.Element + action?: () => JSX.Element } export const ParticipantsCollapsableList = ({ heading, participants, renderParticipant, + action, }: ParticipantsCollapsableListProps) => { const { t } = useTranslation('rooms') const [isOpen, setIsOpen] = useState(true) @@ -98,6 +100,7 @@ export const ParticipantsCollapsableList = ({ {isOpen && ( + {action && action()} {participants.map((participant) => renderParticipant(participant))} )} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index 16ca082b..02e90f1d 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -11,6 +11,7 @@ import { allParticipantRoomEvents } from '@/features/rooms/livekit/constants/eve import { ParticipantListItem } from '@/features/rooms/livekit/components/controls/Participants/ParticipantListItem' import { ParticipantsCollapsableList } from '@/features/rooms/livekit/components/controls/Participants/ParticipantsCollapsableList' import { HandRaisedListItem } from '@/features/rooms/livekit/components/controls/Participants/HandRaisedListItem' +import { LowerAllHandsButton } from '@/features/rooms/livekit/components/controls/Participants/LowerAllHandsButton' // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short. export const ParticipantsList = () => { @@ -101,6 +102,9 @@ export const ParticipantsList = () => { renderParticipant={(participant) => ( )} + action={() => ( + + )} /> )} diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 63fb9487..f9d85d01 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -68,6 +68,7 @@ "cancel": "" }, "raisedHands": "", - "lowerParticipantHand": "" + "lowerParticipantHand": "", + "lowerParticipantsHand": "" } } diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index f8bc9039..633a3baf 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -68,6 +68,7 @@ "cancel": "Cancel" }, "raisedHands": "Raised hands", - "lowerParticipantHand": "Lower {{name}}'s hand" + "lowerParticipantHand": "Lower {{name}}'s hand", + "lowerParticipantsHand": "Lower all hands" } } diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 27b79e00..29008235 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -68,6 +68,7 @@ "cancel": "Annuler" }, "raisedHands": "Mains levées", - "lowerParticipantHand": "Baisser la main de {{name}}" + "lowerParticipantHand": "Baisser la main de {{name}}", + "lowerParticipantsHand": "Baisser la main de tous les participants" } } From 1238ffdb6189a2e3a27d351e8350dc392426df4b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 3 Sep 2024 13:37:46 +0200 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=93=B1(frontend)=20enable=20Y-scrol?= =?UTF-8?q?l=20on=20participants=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Responsiveness issue. Fixed it, to allow user scrolling the participants list. --- .../Participants/ParticipantsList.tsx | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index 02e90f1d..f20da550 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -78,43 +78,41 @@ export const ParticipantsList = () => { - - {t('participants.subheading').toUpperCase()} - - {raisedHandParticipants.length > 0 && ( -
+ - ( - - )} - action={() => ( - - )} - /> -
- )} - ( - + {t('participants.subheading').toUpperCase()} + + {raisedHandParticipants.length > 0 && ( +
+ ( + + )} + action={() => ( + + )} + /> +
)} - /> + ( + + )} + /> +
) } From f05c2f81aef0d0b2b1c2200f802afa850454db6f Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 3 Sep 2024 14:06:46 +0200 Subject: [PATCH 07/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20extract=20?= =?UTF-8?q?raised=20hand=20logic=20in=20a=20reusable=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encapsulate code responsible for toggling hand, and determining the hand state based on participant metadata. This gonna be reused across the codebase. --- .../components/controls/HandToggle.tsx | 23 +++++++------------ .../rooms/livekit/hooks/useRaisedHand.tsx | 22 ++++++++++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.tsx diff --git a/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx index 92bd3cb5..db917c4c 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/HandToggle.tsx @@ -2,25 +2,18 @@ import { useTranslation } from 'react-i18next' import { RiHand } from '@remixicon/react' import { ToggleButton } from '@/primitives' import { css } from '@/styled-system/css' -import { - useLocalParticipant, - useParticipantInfo, -} from '@livekit/components-react' +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 { metadata } = useParticipantInfo({ participant: localParticipant }) + const { isHandRaised, toggleRaisedHand } = useRaisedHand({ + participant: localParticipant, + }) - const parsedMetadata = JSON.parse(metadata || '{}') - - const sendRaise = () => { - parsedMetadata.raised = !parsedMetadata.raised - localParticipant.setMetadata(JSON.stringify(parsedMetadata)) - } - - const label = parsedMetadata.raised + const label = isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise') @@ -36,8 +29,8 @@ export const HandToggle = () => { legacyStyle aria-label={label} tooltip={label} - isSelected={parsedMetadata.raised} - onPress={() => sendRaise()} + isSelected={isHandRaised} + onPress={() => toggleRaisedHand()} > diff --git a/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.tsx b/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.tsx new file mode 100644 index 00000000..a2539bb0 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useRaisedHand.tsx @@ -0,0 +1,22 @@ +import { LocalParticipant, Participant } from 'livekit-client' +import { useParticipantInfo } from '@livekit/components-react' +import { isLocal } from '@/utils/livekit' + +type useRaisedHandProps = { + participant: Participant +} + +export function useRaisedHand({ participant }: useRaisedHandProps) { + const { metadata } = useParticipantInfo({ participant }) + const parsedMetadata = JSON.parse(metadata || '{}') + + const toggleRaisedHand = () => { + if (isLocal(participant)) { + parsedMetadata.raised = !parsedMetadata.raised + const localParticipant = participant as LocalParticipant + localParticipant.setMetadata(JSON.stringify(parsedMetadata)) + } + } + + return { isHandRaised: parsedMetadata.raised, toggleRaisedHand } +} From 7bc0b5ccf9a6db03043807bc59bd0d26393243ec Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 3 Sep 2024 14:16:21 +0200 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=92=A9(frontend)=20indicate=20when?= =?UTF-8?q?=20a=20participant=20raises=20her=20hand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Height was varying when muting/unmuting the mic, so I added a minimum height to avoid layout shift. Proof of concept for the future UX: a raised hand would be placed on the left side of the participant's name. Having the mic indicator and raised hand side by side is temporary and totally undesired (it feels super weird). I'll refactor the whole UX of the participant tile in a dedicated PR. --- .../livekit/components/ParticipantTile.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx b/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx index 182bee68..8e968148 100644 --- a/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx @@ -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<{ @@ -84,6 +86,10 @@ export const ParticipantTile: ( [trackReference, layoutContext] ) + const { isHandRaised } = useRaisedHand({ + participant: trackReference.participant, + }) + return (
@@ -113,9 +119,23 @@ export const ParticipantTile: ( />
-
+
{trackReference.source === Track.Source.Camera ? ( <> + {isHandRaised && ( + + )} {isEncrypted && ( )} From 80f273561ff60df8eb6ab925b41268a3c1d13f7c Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 3 Sep 2024 14:34:45 +0200 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=A9=B9(frontend)=20fix=20color=20co?= =?UTF-8?q?ntrast=20when=20toggling=20a=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copied legacy style from the screen share control. Still imperfect, but at least it's visually more "comfy". --- src/frontend/src/primitives/buttonRecipe.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/primitives/buttonRecipe.ts b/src/frontend/src/primitives/buttonRecipe.ts index 52b2561b..a40f3064 100644 --- a/src/frontend/src/primitives/buttonRecipe.ts +++ b/src/frontend/src/primitives/buttonRecipe.ts @@ -140,6 +140,7 @@ export const buttonRecipe = cva({ legacyStyle: { true: { borderColor: 'gray.400', + transition: 'border 200ms, background 200ms, color 200ms', '&[data-hovered]': { borderColor: 'gray.500', }, @@ -147,10 +148,12 @@ export const buttonRecipe = cva({ borderColor: 'gray.500', }, '&[data-selected]': { - background: '#e5e7eb', - borderColor: 'gray.400', + backgroundColor: '#1d4ed8', + color: 'white', + borderColor: 'gray.500', '&[data-hovered]': { - backgroundColor: 'gray.300', + borderColor: '#6b7280', + backgroundColor: '#1e40af', }, }, }, From a4df83c269ddfcf483e9a9da71d73e4f2f1d46c5 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 3 Sep 2024 15:00:24 +0200 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=92=AB(frontend)=20wave=20icon=20wh?= =?UTF-8?q?en=20a=20participant=20raises=20her=20hand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor enhancement, add some CSS animation to wave a newly raised hand. Inspired by GMeet. --- src/frontend/panda.config.ts | 6 ++++++ .../features/rooms/livekit/components/ParticipantTile.tsx | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/frontend/panda.config.ts b/src/frontend/panda.config.ts index 088017dc..73328cfc 100644 --- a/src/frontend/panda.config.ts +++ b/src/frontend/panda.config.ts @@ -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. diff --git a/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx b/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx index 8e968148..2db3aace 100644 --- a/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ParticipantTile.tsx @@ -133,6 +133,9 @@ export const ParticipantTile: ( size={16} style={{ marginInlineEnd: '.25rem', // fixme - match TrackMutedIndicator styling + animationDuration: '300ms', + animationName: 'wave_hand', + animationIterationCount: '2', }} /> )}