diff --git a/CHANGELOG.md b/CHANGELOG.md index bd853728..5755c3bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ### Changed - ♿️(frontend) adjust visual-only tooltip a11y labels #910 +- ♿️(frontend) sr pin/unpin announcements with dedicated messages #898 - ♿(frontend) adjust sr announcements for idle disconnect timer #908 ### Fixed diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index cee1eece..a0aa8695 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -5,9 +5,8 @@ import { isWeb, log, } from '@livekit/components-core' -import { RoomEvent, Track } from 'livekit-client' -import * as React from 'react' -import { useState } from 'react' +import { Participant, RoomEvent, Track } from 'livekit-client' +import React, { useCallback, useRef, useState, useEffect } from 'react' import { ConnectionStateToast, FocusLayoutContainer, @@ -16,7 +15,9 @@ import { usePinnedTracks, useTracks, useCreateLayoutContext, + useRoomContext, } from '@livekit/components-react' +import { useTranslation } from 'react-i18next' import { ControlBar } from './ControlBar/ControlBar' import { styled } from '@/styled-system/jsx' @@ -37,6 +38,7 @@ import { Subtitles } from '@/features/subtitle/component/Subtitles' import { CarouselLayout } from '../components/layout/CarouselLayout' import { GridLayout } from '../components/layout/GridLayout' import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal' +import { getParticipantName } from '@/features/rooms/utils/getParticipantName' const LayoutWrapper = styled( 'div', @@ -89,7 +91,22 @@ export interface VideoConferenceProps */ export function VideoConference({ ...props }: VideoConferenceProps) { const lastAutoFocusedScreenShareTrack = - React.useRef(null) + useRef(null) + const lastPinnedParticipantIdentityRef = useRef(null) + const [pinAnnouncement, setPinAnnouncement] = useState('') + const { t } = useTranslation('rooms', { keyPrefix: 'pinAnnouncements' }) + const { t: tRooms } = useTranslation('rooms') + const room = useRoomContext() + + const getAnnouncementName = useCallback( + (participant?: Participant | null) => { + if (!participant) return tRooms('participants.unknown') + return participant.isLocal + ? tRooms('participants.you') + : getParticipantName(participant) + }, + [tRooms] + ) useConnectionObserver() useVideoResolutionSubscription() @@ -115,9 +132,61 @@ export function VideoConference({ ...props }: VideoConferenceProps) { (track) => !isEqualTrackRef(track, focusTrack) ) + // handle pin announcements + + useEffect(() => { + const participant = focusTrack?.participant + + // 1. unpin + if (!participant) { + if (!lastPinnedParticipantIdentityRef.current) return + + const lastIdentity = lastPinnedParticipantIdentityRef.current + const lastParticipant = + room.localParticipant.identity === lastIdentity + ? room.localParticipant + : room.remoteParticipants.get(lastIdentity) + const announcementName = getAnnouncementName(lastParticipant) + + setPinAnnouncement( + lastParticipant?.isLocal + ? t('self.unpin') + : t('unpin', { + name: announcementName, + }) + ) + + lastPinnedParticipantIdentityRef.current = null + return + } + + // 2. same pin → do nothing + if (lastPinnedParticipantIdentityRef.current === participant.identity) { + return + } + + // 3. new pin + const participantName = participant.isLocal + ? tRooms('participants.you') + : getParticipantName(participant) + + lastPinnedParticipantIdentityRef.current = participant.identity + + setPinAnnouncement( + participant.isLocal ? t('self.pin') : t('pin', { name: participantName }) + ) + }, [ + focusTrack, + getAnnouncementName, + room.localParticipant, + room.remoteParticipants, + t, + tRooms, + ]) + /* eslint-disable react-hooks/exhaustive-deps */ // Code duplicated from LiveKit; this warning will be addressed in the refactoring. - React.useEffect(() => { + useEffect(() => { // If screen share tracks are published, and no pin is set explicitly, auto set the screen share. if ( screenShareTracks.some((track) => track.publication.isSubscribed) && @@ -188,6 +257,14 @@ export function VideoConference({ ...props }: VideoConferenceProps) { value={layoutContext} // onPinChange={handleFocusStateChange} > +
+ {pinAnnouncement} +
setIsShareErrorVisible(false)} diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index 61522d74..f99799e6 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -505,6 +505,7 @@ "participants": { "subheading": "Im Raum", "you": "Du", + "unknown": "Unbekannter Teilnehmer", "host": "Host", "contributors": "Mitwirkende", "collapsable": { @@ -552,6 +553,14 @@ "ariaLabel": "Hefte {{name}} an" } }, + "pinAnnouncements": { + "pin": "Das Video von {{name}} ist angeheftet.", + "unpin": "Das Video von {{name}} ist nicht mehr angeheftet.", + "self": { + "pin": "Dein Video ist angeheftet.", + "unpin": "Dein Video ist nicht mehr angeheftet." + } + }, "recordingStateToast": { "transcript": { "started": "Transkription läuft", diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index 27cf2c85..efb18f74 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -505,6 +505,7 @@ "participants": { "subheading": "In room", "you": "You", + "unknown": "Unknown participant", "host": "Host", "contributors": "Contributors", "collapsable": { @@ -552,6 +553,14 @@ "ariaLabel": "Pin {{name}}" } }, + "pinAnnouncements": { + "pin": "The video of {{name}} is pinned.", + "unpin": "The video of {{name}} is no longer pinned.", + "self": { + "pin": "Your video is pinned.", + "unpin": "Your video is no longer pinned." + } + }, "recordingStateToast": { "transcript": { "started": "Transcribing", diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 59acc640..0435bc06 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -505,6 +505,7 @@ "participants": { "subheading": "Dans la réunion", "you": "Vous", + "unknown": "Participant inconnu", "contributors": "Contributeurs", "host": "Organisateur de la réunion", "collapsable": { @@ -552,6 +553,14 @@ "ariaLabel": "Épingler {{name}}" } }, + "pinAnnouncements": { + "pin": "La vidéo de {{name}} est épinglée.", + "unpin": "La vidéo de {{name}} n’est plus épinglée.", + "self": { + "pin": "Votre vidéo est épinglée.", + "unpin": "Votre vidéo n’est plus épinglée." + } + }, "recordingStateToast": { "transcript": { "started": "Transcription en cours", diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index 7ae75cfb..fe702a04 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -505,6 +505,7 @@ "participants": { "subheading": "In de ruimte", "you": "U", + "unknown": "Onbekende deelnemer", "host": "Host", "contributors": "Deelnemers", "collapsable": { @@ -552,6 +553,14 @@ "ariaLabel": "Maak {{name}} vast" } }, + "pinAnnouncements": { + "pin": "De video van {{name}} is vastgezet.", + "unpin": "De video van {{name}} is niet meer vastgezet.", + "self": { + "pin": "Uw video is vastgezet.", + "unpin": "Uw video is niet meer vastgezet." + } + }, "recordingStateToast": { "transcript": { "started": "Transcriptie bezig",