From 447def54c860ee7120f5fcba72a70716345b5d12 Mon Sep 17 00:00:00 2001 From: Calinteodor Date: Tue, 20 Jan 2026 13:33:46 +0200 Subject: [PATCH 01/50] feat(chat/native): add Closed Captions tab (#16787) * Added CC tab inside Chat screen and some UI fixes on mobile. --- .../base/modal/components/JitsiScreen.tsx | 2 +- react/features/chat/actions.native.ts | 2 +- .../components/AbstractClosedCaptions.tsx | 111 ++++++++++++++ .../chat/components/native/ChatButton.ts | 2 +- .../chat/components/native/ClosedCaptions.tsx | 125 +++++++++++++++ .../components/native/MessageContainer.tsx | 8 +- .../native/PrivateMessageButton.tsx | 4 +- .../components/native/SubtitleMessage.tsx | 45 ++++++ .../chat/components/native/SubtitlesGroup.tsx | 40 +++++ .../native/SubtitlesMessagesContainer.tsx | 105 +++++++++++++ .../features/chat/components/native/styles.ts | 143 +++++++++++++++++- .../chat/components/web/ClosedCaptionsTab.tsx | 98 ++---------- .../components/TabBarLabelCounter.tsx | 2 +- .../chat/components/ChatAndPollsNavigator.tsx | 62 -------- .../chat/components/ChatNavigator.tsx | 88 +++++++++++ .../ConferenceNavigationContainer.tsx | 29 +++- .../SettingsNavigationContainer.tsx | 2 + react/features/mobile/navigation/routes.ts | 7 +- .../polls/components/native/PollsList.tsx | 54 ++++--- .../polls/components/native/styles.ts | 23 +-- .../components/native/LanguageSelectView.tsx | 11 +- 21 files changed, 757 insertions(+), 206 deletions(-) create mode 100644 react/features/chat/components/AbstractClosedCaptions.tsx create mode 100644 react/features/chat/components/native/ClosedCaptions.tsx create mode 100644 react/features/chat/components/native/SubtitleMessage.tsx create mode 100644 react/features/chat/components/native/SubtitlesGroup.tsx create mode 100644 react/features/chat/components/native/SubtitlesMessagesContainer.tsx delete mode 100644 react/features/mobile/navigation/components/chat/components/ChatAndPollsNavigator.tsx create mode 100644 react/features/mobile/navigation/components/chat/components/ChatNavigator.tsx diff --git a/react/features/base/modal/components/JitsiScreen.tsx b/react/features/base/modal/components/JitsiScreen.tsx index c83b9cffc60e..59a5cf2c0fe9 100644 --- a/react/features/base/modal/components/JitsiScreen.tsx +++ b/react/features/base/modal/components/JitsiScreen.tsx @@ -17,7 +17,7 @@ interface IProps { /** * The children component(s) of the Modal, to be rendered. */ - children: React.ReactNode; + children?: React.ReactNode; /** * Additional style to be appended to the KeyboardAvoidingView content container. diff --git a/react/features/chat/actions.native.ts b/react/features/chat/actions.native.ts index 3a57d63662a6..83816e1a45fa 100644 --- a/react/features/chat/actions.native.ts +++ b/react/features/chat/actions.native.ts @@ -22,7 +22,7 @@ export function openChat(participant?: IParticipant | undefined | Object, disabl if (disablePolls) { navigate(screen.conference.chat); } else { - navigate(screen.conference.chatandpolls.main); + navigate(screen.conference.chatTabs.main); } dispatch(setFocusedTab(ChatTabs.CHAT)); diff --git a/react/features/chat/components/AbstractClosedCaptions.tsx b/react/features/chat/components/AbstractClosedCaptions.tsx new file mode 100644 index 000000000000..a02ebb51cce7 --- /dev/null +++ b/react/features/chat/components/AbstractClosedCaptions.tsx @@ -0,0 +1,111 @@ +import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { IReduxState } from '../../app/types'; +import { openDialog } from '../../base/dialog/actions'; +import { IMessageGroup, groupMessagesBySender } from '../../base/util/messageGrouping'; +// @ts-ignore +import { StartRecordingDialog } from '../../recording/components/Recording'; +import { setRequestingSubtitles } from '../../subtitles/actions.any'; +import { canStartSubtitles } from '../../subtitles/functions.any'; +import { ISubtitle } from '../../subtitles/types'; +import { isTranscribing } from '../../transcribing/functions'; + +export type AbstractProps = { + canStartSubtitles: boolean; + filteredSubtitles: ISubtitle[]; + groupedSubtitles: IMessageGroup[]; + isButtonPressed: boolean; + isTranscribing: boolean; + startClosedCaptions: () => void; +}; + +const AbstractClosedCaptions = (Component: ComponentType) => () => { + const dispatch = useDispatch(); + const subtitles = useSelector((state: IReduxState) => state['features/subtitles'].subtitlesHistory); + const language = useSelector((state: IReduxState) => state['features/subtitles']._language); + const selectedLanguage = language?.replace('translation-languages:', ''); + const _isTranscribing = useSelector(isTranscribing); + const _canStartSubtitles = useSelector(canStartSubtitles); + const [ isButtonPressed, setButtonPressed ] = useState(false); + const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError); + const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) => + state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription); + + const filteredSubtitles = useMemo(() => { + // First, create a map of transcription messages by message ID + const transcriptionMessages = new Map( + subtitles + .filter(s => s.isTranscription) + .map(s => [ s.id, s ]) + ); + + if (!selectedLanguage) { + // When no language is selected, show all original transcriptions + return Array.from(transcriptionMessages.values()); + } + + // Then, create a map of translation messages by message ID + const translationMessages = new Map( + subtitles + .filter(s => !s.isTranscription && s.language === selectedLanguage) + .map(s => [ s.id, s ]) + ); + + // When a language is selected, for each transcription message: + // 1. Use its translation if available + // 2. Fall back to the original transcription if no translation exists + return Array.from(transcriptionMessages.values()) + .filter((m: ISubtitle) => !m.interim) + .map(m => translationMessages.get(m.id) ?? m); + }, [ subtitles, selectedLanguage ]); + + const groupedSubtitles = useMemo(() => + groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]); + + const startClosedCaptions = useCallback(() => { + if (isAsyncTranscriptionEnabled) { + dispatch(openDialog('StartRecordingDialog', StartRecordingDialog, { + recordAudioAndVideo: false + })); + } else { + if (isButtonPressed) { + return; + } + dispatch(setRequestingSubtitles(true, false, null)); + setButtonPressed(true); + } + + }, [ isAsyncTranscriptionEnabled, dispatch, isButtonPressed, openDialog, setButtonPressed ]); + + useEffect(() => { + if (subtitlesError && isButtonPressed && !isAsyncTranscriptionEnabled) { + setButtonPressed(false); + } + }, [ subtitlesError, isButtonPressed, isAsyncTranscriptionEnabled ]); + + useEffect(() => { + if (!_isTranscribing && isButtonPressed && !isAsyncTranscriptionEnabled) { + setButtonPressed(false); + } + }, [ _isTranscribing, isButtonPressed, isAsyncTranscriptionEnabled ]); + + useEffect(() => { + if (isButtonPressed && !isAsyncTranscriptionEnabled) { + setButtonPressed(false); + } + }, [ isButtonPressed, isAsyncTranscriptionEnabled ]); + + return ( + + ); +}; + +export default AbstractClosedCaptions; + diff --git a/react/features/chat/components/native/ChatButton.ts b/react/features/chat/components/native/ChatButton.ts index dcf50e4e1228..5a2146abcb49 100644 --- a/react/features/chat/components/native/ChatButton.ts +++ b/react/features/chat/components/native/ChatButton.ts @@ -43,7 +43,7 @@ class ChatButton extends AbstractButton { override _handleClick() { this.props._isPollsDisabled ? navigate(screen.conference.chat) - : navigate(screen.conference.chatandpolls.main); + : navigate(screen.conference.chatTabs.main); } /** diff --git a/react/features/chat/components/native/ClosedCaptions.tsx b/react/features/chat/components/native/ClosedCaptions.tsx new file mode 100644 index 000000000000..855c1beeb157 --- /dev/null +++ b/react/features/chat/components/native/ClosedCaptions.tsx @@ -0,0 +1,125 @@ +import { useNavigation } from '@react-navigation/native'; +import React, { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TouchableHighlight, View, ViewStyle } from 'react-native'; +import { Text } from 'react-native-paper'; +import { useSelector } from 'react-redux'; + +import { IReduxState } from '../../../app/types'; +import Icon from '../../../base/icons/components/Icon'; +import { IconArrowRight, IconSubtitles } from '../../../base/icons/svg'; +import JitsiScreen from '../../../base/modal/components/JitsiScreen'; +import { StyleType } from '../../../base/styles/functions.any'; +import BaseTheme from '../../../base/ui/components/BaseTheme.native'; +import Button from '../../../base/ui/components/native/Button'; +import { BUTTON_TYPES } from '../../../base/ui/constants.native'; +import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter'; +import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef'; +import { screen } from '../../../mobile/navigation/routes'; +import { ChatTabs } from '../../constants'; +import AbstractClosedCaptions, { AbstractProps } from '../AbstractClosedCaptions'; + +import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer'; +import { closedCaptionsStyles } from './styles'; + +/** + * Component that displays the closed captions interface. + * + * @returns {JSX.Element} - The ClosedCaptions component. + */ +const ClosedCaptions = ({ + canStartSubtitles, + filteredSubtitles, + groupedSubtitles, + isButtonPressed, + isTranscribing, + startClosedCaptions +}: AbstractProps): JSX.Element => { + const navigation = useNavigation(); + const { t } = useTranslation(); + const isCCTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CLOSED_CAPTIONS); + const selectedLanguage = useSelector((state: IReduxState) => state['features/subtitles']._language); + const navigateToLanguageSelect = useCallback(() => { + navigate(screen.conference.subtitles); + }, [ navigation, screen ]); + + useEffect(() => { + navigation?.setOptions({ + tabBarLabel: () => ( + + ) + }); + }, [ isCCTabFocused, navigation, t ]); + + const getContentContainerStyle = () => { + if (isTranscribing) { + return closedCaptionsStyles.transcribingContainer as StyleType; + } + + return closedCaptionsStyles.emptyContentContainer as StyleType; + }; + + const renderContent = () => { + if (!isTranscribing) { + if (canStartSubtitles) { + return ( + +