diff --git a/Project/src/App.css b/Project/src/App.css index ade3a45..8f487f0 100644 --- a/Project/src/App.css +++ b/Project/src/App.css @@ -1148,6 +1148,13 @@ div.volumeVisualizer::before { transform: translateZ(0); } +.scrollable-rtt-container { + overflow: auto; + max-height: 300px; + display: flex; + flex-direction: column-reverse; +} + .custom-video-effects-buttons:not(.outgoing) { display: flex; position: absolute; diff --git a/Project/src/MakeCall/CallCaption.js b/Project/src/MakeCall/CallCaption.js index cf429a8..9884211 100644 --- a/Project/src/MakeCall/CallCaption.js +++ b/Project/src/MakeCall/CallCaption.js @@ -5,9 +5,12 @@ import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; // CallCaption react function component const CallCaption = ({ call }) => { const [captionsFeature, setCaptionsFeature] = useState(call.feature(Features.Captions)); + const [capabilitiesFeature, setCapabilitiesFeature] = useState(call.feature(Features.Capabilities)); const [captions, setCaptions] = useState(captionsFeature.captions); const [currentSpokenLanguage, setCurrentSpokenLanguage] = useState(captions.activeSpokenLanguage); - const [currentCaptionLanguage, setCurrentCaptionLanguage] = useState(captions.activeCaptionLanguage); + const [currentCaptionLanguage, setCurrentCaptionLanguage] = useState(null); + let captionLanguageCurrent = null; + useEffect(() => { try { @@ -23,7 +26,7 @@ const CallCaption = ({ call }) => { captions.off('CaptionsActiveChanged', captionsActiveHandler); captions.off('CaptionsReceived', captionsReceivedHandler); captions.off('SpokenLanguageChanged', activeSpokenLanguageHandler); - if (captions.captionsType === 'TeamsCaptions') { + if (captions.kind === 'TeamsCaptions' && capabilitiesFeature.capabilities.setCaptionLanguage?.isPresent) { captions.off('CaptionLanguageChanged', activeCaptionLanguageHandler); } }; @@ -37,7 +40,12 @@ const CallCaption = ({ call }) => { captions.on('CaptionsActiveChanged', captionsActiveHandler); captions.on('CaptionsReceived', captionsReceivedHandler); captions.on('SpokenLanguageChanged', activeSpokenLanguageHandler); - if (captions.captionsType === 'TeamsCaptions') { + capabilitiesFeature.on('CapabilitiesChanged', (value) => { + if (value.newValue.setCaptionLanguage) { + setCapabilitiesFeature(call.feature(Features.Capabilities)); + } + }); + if (captions.kind === 'TeamsCaptions' && capabilitiesFeature.capabilities.setCaptionLanguage?.isPresent) { captions.on('CaptionLanguageChanged', activeCaptionLanguageHandler); } } catch (e) { @@ -55,46 +63,49 @@ const CallCaption = ({ call }) => { const captionsActiveHandler = () => { console.log('CaptionsActiveChanged: ', captions.isCaptionsFeatureActive); + setCurrentSpokenLanguage(captions.activeSpokenLanguage); + setCurrentCaptionLanguage(captions.activeCaptionLanguage); } const activeSpokenLanguageHandler = () => { setCurrentSpokenLanguage(captions.activeSpokenLanguage); } const activeCaptionLanguageHandler = () => { setCurrentCaptionLanguage(captions.activeCaptionLanguage); + captionLanguageCurrent = captions.activeCaptionLanguage; } const captionsReceivedHandler = (captionData) => { - let mri = ''; - if (captionData.speaker.identifier.kind === 'communicationUser') { - mri = captionData.speaker.identifier.communicationUserId; - } else if (captionData.speaker.identifier.kind === 'microsoftTeamsUser') { - mri = captionData.speaker.identifier.microsoftTeamsUserId; - } else if (captionData.speaker.identifier.kind === 'phoneNumber') { - mri = captionData.speaker.identifier.phoneNumber; - } - - let captionAreasContainer = document.getElementById('captionsArea'); - const newClassName = `prefix${mri.replace(/:/g, '').replace(/-/g, '').replace(/\+/g, '')}`; - const captionText = `${captionData.timestamp.toUTCString()} - ${captionData.speaker.displayName}: ${captionData.captionText ?? captionData.spokenText}`; - - let foundCaptionContainer = captionAreasContainer.querySelector(`.${newClassName}[isNotFinal='true']`); - if (!foundCaptionContainer) { - let captionContainer = document.createElement('div'); - captionContainer.setAttribute('isNotFinal', 'true'); - captionContainer.style['borderBottom'] = '1px solid'; - captionContainer.style['whiteSpace'] = 'pre-line'; - captionContainer.textContent = captionText; - captionContainer.classList.add(newClassName); - captionContainer.classList.add('caption-item') - - captionAreasContainer.appendChild(captionContainer); - - } else { - foundCaptionContainer.textContent = captionText; - - if (captionData.resultType === 'Final') { - foundCaptionContainer.setAttribute('isNotFinal', 'false'); + if (!captionLanguageCurrent || captionLanguageCurrent === captionData.captionLanguage) { + let mri = ''; + switch (captionData.speaker.identifier.kind) { + case 'communicationUser': { mri = captionData.speaker.identifier.communicationUserId; break; } + case 'microsoftTeamsUser': { mri = captionData.speaker.identifier.microsoftTeamsUserId; break; } + case 'phoneNumber': { mri = captionData.speaker.identifier.phoneNumber; break; } + } + let captionAreasContainer = document.getElementById('captionsArea'); + const newClassName = `prefix${mri.replace(/:/g, '').replace(/-/g, '').replace(/\+/g, '')}`; + const captionText = `${captionData.timestamp.toUTCString()} + ${captionData.speaker.displayName ?? mri}: ${captionData.captionText ?? captionData.spokenText}`; + + let foundCaptionContainer = captionAreasContainer.querySelector(`.${newClassName}[isNotFinal='true']`); + + if (!foundCaptionContainer) { + let captionContainer = document.createElement('div'); + captionContainer.setAttribute('isNotFinal', 'true'); + captionContainer.style['borderBottom'] = '1px solid'; + captionContainer.style['whiteSpace'] = 'pre-line'; + captionContainer.textContent = captionText; + captionContainer.classList.add(newClassName); + captionContainer.classList.add('caption-item') + + captionAreasContainer.appendChild(captionContainer); + + } else { + foundCaptionContainer.textContent = captionText; + + if (captionData.resultType === 'Final') { + foundCaptionContainer.setAttribute('isNotFinal', 'false'); + } } } }; @@ -113,7 +124,7 @@ const CallCaption = ({ call }) => { onChange={spokenLanguageSelectionChanged} label={'Spoken Language'} options={keyedSupportedSpokenLanguages} - styles={{ label: {color: '#edebe9'}, dropdown: { width: 100 } }} + styles={{ label: {color: '#edebe9'}, dropdown: { width: 100 }, root: {paddingBottom: '1rem'} }} /> } @@ -131,14 +142,14 @@ const CallCaption = ({ call }) => { onChange={captionLanguageSelectionChanged} label={'Caption Language'} options={keyedSupportedCaptionLanguages} - styles={{ label: {color: '#edebe9'}, dropdown: { width: 100, overflow: 'scroll' } }} + styles={{ label: {color: '#edebe9'}, dropdown: { width: 100, overflow: 'scroll' }, root: {paddingBottom: '1rem'} }} /> } return ( <> {captions && } - {captions && captions.captionsType === 'TeamsCaptions' && } + {captions && captions.kind === 'TeamsCaptions' && capabilitiesFeature.capabilities.setCaptionLanguage?.isPresent && }
diff --git a/Project/src/MakeCall/CallCard.js b/Project/src/MakeCall/CallCard.js index af5f27d..1f382f0 100644 --- a/Project/src/MakeCall/CallCard.js +++ b/Project/src/MakeCall/CallCard.js @@ -20,6 +20,7 @@ import CallCaption from "./CallCaption"; import Lobby from "./Lobby"; import { ParticipantMenuOptions } from './ParticipantMenuOptions'; import MediaConstraint from './MediaConstraint'; +import RealTimeTextCard from "./RealTimeTextCard"; export default class CallCard extends React.Component { constructor(props) { @@ -40,6 +41,9 @@ export default class CallCard extends React.Component { this.raiseHandFeature = this.call.feature(Features.RaiseHand); this.capabilitiesFeature = this.call.feature(Features.Capabilities); this.capabilities = this.capabilitiesFeature.capabilities; + if (Features.RealTimeText) { + this.realTimeTextFeature = this.call.feature(Features.RealTimeText); + } this.dominantSpeakersFeature = this.call.feature(Features.DominantSpeakers); this.recordingFeature = this.call.feature(Features.Recording); this.transcriptionFeature = this.call.feature(Features.Transcription); @@ -86,6 +90,9 @@ export default class CallCard extends React.Component { callMessage: undefined, dominantSpeakerMode: false, captionOn: false, + realTimeTextOn: false, + firstRealTimeTextReceivedorSent: false, + showCanNotHideorCloseRealTimeTextBanner: false, dominantRemoteParticipant: undefined, logMediaStats: false, sentResolution: '', @@ -111,6 +118,10 @@ export default class CallCard extends React.Component { this.isSetCallConstraints = this.call.setConstraints !== undefined; } + setFirstRealTimeTextReceivedorSent = (state) => { + this.setState({ firstRealTimeTextReceivedorSent: state }); + } + componentWillUnmount() { this.call.off('stateChanged', () => { }); this.deviceManager.off('videoDevicesUpdated', () => { }); @@ -468,6 +479,7 @@ export default class CallCard extends React.Component { this.recordingFeature.on('isRecordingActiveChanged', this.isRecordingActiveChangedHandler); this.transcriptionFeature.on('isTranscriptionActiveChanged', this.isTranscriptionActiveChangedHandler); this.lobby?.on('lobbyParticipantsUpdated', this.lobbyParticipantsUpdatedHandler); + this.realTimeTextFeature?.on('realTimeTextReceived', this.realTimeTextReceivedHandler); } } @@ -649,6 +661,62 @@ export default class CallCard extends React.Component { this.capabilities = this.capabilitiesFeature.capabilities; } + realTimeTextReceivedHandler = (rttData) => { + this.setState({ realTimeTextOn: true }); + if (!this.state.firstRealTimeTextReceivedorSent) { + this.setState({ firstRealTimeTextReceivedorSent: true }); + } + if (rttData) { + + let mri = ''; + let displayName = ''; + switch (rttData.sender.identifier.kind) { + case 'communicationUser': { mri = rttData.sender.identifier.communicationUserId; displayName = rttData.sender.displayName; break; } + case 'microsoftTeamsUser': { mri = rttData.sender.identifier.microsoftTeamsUserId; displayName = rttData.sender.displayName; break; } + case 'phoneNumber': { mri = rttData.sender.identifier.phoneNumber; displayName = rttData.sender.displayName; break; } + } + + let rttAreaContainer = document.getElementById('rttArea'); + + const newClassName = `prefix${mri.replace(/:/g, '').replace(/-/g, '').replace(/\+/g, '')}`; + const rttText = `${(rttData.receivedTimestamp).toUTCString()} ${displayName ?? mri} isTyping: `; + + let foundRTTContainer = rttAreaContainer.querySelector(`.${newClassName}[isNotFinal='true']`); + + if (!foundRTTContainer) { + if (rttData.text.trim() === '') { + return + } + let rttContainer = document.createElement('div'); + rttContainer.setAttribute('isNotFinal', 'true'); + rttContainer.style['borderBottom'] = '1px solid'; + rttContainer.style['whiteSpace'] = 'pre-line'; + rttContainer.textContent = rttText + rttData.text; + rttContainer.classList.add(newClassName); + + rttAreaContainer.appendChild(rttContainer); + + setTimeout(() => { + rttAreaContainer.removeChild(rttContainer); + }, 40000); + } else { + if (rttData.text.trim() === '') { + rttAreaContainer.removeChild(foundRTTContainer); + } + if (rttData.resultType === 'Final') { + foundRTTContainer.setAttribute('isNotFinal', 'false'); + foundRTTContainer.textContent = foundRTTContainer.textContent.replace(' isTyping', ''); + if (rttData.isLocal) { + let rttTextField = document.getElementById('rttTextField'); + rttTextField.value = null; + } + } else { + foundRTTContainer.textContent = rttText + rttData.text; + } + } + } + } + dominantSpeakersChanged = () => { const dominantSpeakersMris = this.dominantSpeakersFeature.dominantSpeakers.speakersList; const remoteParticipants = dominantSpeakersMris.map(dominantSpeakerMri => { @@ -1455,6 +1523,27 @@ export default class CallCard extends React.Component { } + { Features.RealTimeText && + + }
} + { + Features.RealTimeText && this.state.realTimeTextOn && +
+
+

RealTimeText

+
+
+ { + this.state.realTimeTextOn && + this.state.firstRealTimeTextReceivedorSent && + this.state.showCanNotHideorCloseRealTimeTextBanner && + { this.setState({ showCanNotHideorCloseRealTimeTextBanner: undefined }) }} + dismissButtonAriaLabel="Close"> + Note: RealTimeText can not be closed or hidden after you have sent or received a message. + + } + { + this.state.realTimeTextOn && + + } +
+
+ } { this.state.showDataChannel &&
diff --git a/Project/src/MakeCall/RealTimeTextCard.js b/Project/src/MakeCall/RealTimeTextCard.js new file mode 100644 index 0000000..62544d6 --- /dev/null +++ b/Project/src/MakeCall/RealTimeTextCard.js @@ -0,0 +1,97 @@ +import React, { useEffect, useState } from "react"; +import { Features } from '@azure/communication-calling'; +import { PrimaryButton } from 'office-ui-fabric-react/lib/components/Button'; + +// RealTimeText react function component +const RealTimeTextCard = ({ call, state }) => { + const [realTimeTextFeature, setRealTimeTextFeature] = useState(call.feature(Features.RealTimeText)); + const [rttInputLiveHandler, setRttInputLiveHandler] = useState(false); + + useEffect(() => { + try { + subscribeToSendRealTimeTextLive(); + } + catch(error) { + console.log("RealTimeText not configured for this release version") + } + + return () => { + // cleanup + let rttTextField = document.getElementById('rttTextField'); + rttTextField.removeEventListener('input', subscribeToSendRealTimeTextHelper); + }; + }, []); + + const sendRTT = async () => { + try { + let rttTextField = document.getElementById('rttTextField'); + if (!state.firstRealTimeTextReceivedorSent) { + state.setFirstRealTimeTextReceivedorSent(true); + } + realTimeTextFeature.sendRealTimeText(rttTextField.value, true); + rttTextField.value = null; + } catch (error) { + console.log('ERROR Send RTT failed', error); + } + } + + const sendRealTimeTextLiveHandler = () => { + if (!rttInputLiveHandler) { + try { + let rttTextField = document.getElementById('rttTextField'); + rttTextField.removeEventListener('input', subscribeToSendRealTimeTextHelper); + rttTextField.addEventListener('input', (event) => { + if (!state.firstRealTimeTextReceivedorSent) { + state.setFirstRealTimeTextReceivedorSent(true); + } + realTimeTextFeature.sendRealTimeText(rttTextField.value); + }) + setRttInputLiveHandler(true); + } catch (error) { + console.log('ERROR Send live rtt handler subscription failed', error); + } + } + } + + const subscribeToSendRealTimeTextHelper = () => { + let rttTextField = document.getElementById('rttTextField'); + if (rttTextField.value !== '') { + sendRealTimeTextLiveHandler(); + } + setRttInputLiveHandler(true); + } + + const subscribeToSendRealTimeTextLive = () => { + if (!rttInputLiveHandler) { + try { + let rttTextField = document.getElementById('rttTextField'); + rttTextField.removeEventListener('input', subscribeToSendRealTimeTextHelper); + rttTextField.addEventListener('input', subscribeToSendRealTimeTextHelper); + } catch (error) { + console.log('ERROR setting live rtt handler', error); + } + + } + } + + return ( + <> +
+
+ + + + +
+
+
+
+
+ + ); +}; + +export default RealTimeTextCard;