@@ -4,6 +4,8 @@ import { Stack, FocusZone, Spinner, useTheme } from '@fluentui/react';
4
4
/* @conditional -compile-remove(rtt) */
5
5
import { TextField } from '@fluentui/react' ;
6
6
import React , { useEffect , useRef , useState , useCallback } from 'react' ;
7
+ /* @conditional -compile-remove(rtt) */
8
+ import { useMemo } from 'react' ;
7
9
import { _Caption } from './Caption' ;
8
10
import {
9
11
captionContainerClassName ,
@@ -19,6 +21,8 @@ import { useLocale } from '../localization';
19
21
import { RealTimeText } from './RealTimeText' ;
20
22
/* @conditional -compile-remove(rtt) */
21
23
import { _RTTDisclosureBanner } from './RTTDisclosureBanner' ;
24
+ /* @conditional -compile-remove(rtt) */
25
+ import { sortCaptionsAndRealTimeTexts } from './utils/sortCaptionsAndRealTimeTexts' ;
22
26
23
27
/**
24
28
* @public
@@ -43,21 +47,48 @@ export type CaptionsInformation = {
43
47
userId ?: string ;
44
48
/* @conditional -compile-remove(rtt) */
45
49
/**
46
- * if the caption received is real time text
50
+ * timestamp when the caption was created
51
+ * Please note that this value is essential for determining the order of captions and real time text messages
52
+ * If you are using both captions and real time text, please ensure that the createdTimeStamp is populated
47
53
*/
48
- isRealTimeText ?: boolean ;
49
- /* @conditional -compile-remove(rtt) */
54
+ createdTimeStamp ?: Date ;
55
+ } ;
56
+
57
+ /* @conditional -compile-remove(rtt) */
58
+ /**
59
+ * @beta
60
+ * information required for each line of real time text
61
+ */
62
+ export type RealTimeTextInformation = {
50
63
/**
51
- * if the caption received is a non finalized caption
64
+ * The sequence id of the real time text.
52
65
*/
53
- isPartial ?: boolean ;
54
- /* @conditional -compile-remove(rtt) */
66
+ id : number ;
55
67
/**
56
- * if the caption received is from the local user
68
+ * sender's display name
57
69
*/
58
- isLocalUser ?: boolean ;
70
+ displayName : string ;
71
+ /**
72
+ * id of the sender
73
+ */
74
+ userId ?: string ;
75
+ /**
76
+ * The real time text message.
77
+ */
78
+ message : string ;
79
+ /**
80
+ * if the real time text received is partial
81
+ */
82
+ isTyping : boolean ;
83
+ /**
84
+ * If message originated from the local participant
85
+ */
86
+ isMe : boolean ;
87
+ /**
88
+ * timestamp when the real time text was finalized
89
+ */
90
+ finalizedTimeStamp : Date ;
59
91
} ;
60
-
61
92
/**
62
93
* @public
63
94
* strings for captions banner
@@ -98,6 +129,15 @@ export interface CaptionsBannerProps {
98
129
* Array of captions to be displayed
99
130
*/
100
131
captions : CaptionsInformation [ ] ;
132
+ /* @conditional -compile-remove(rtt) */
133
+ /**
134
+ * Array of finalized and partial real time text messages
135
+ */
136
+ realTimeTexts ?: {
137
+ completedMessages ?: RealTimeTextInformation [ ] ;
138
+ currentInProgress ?: RealTimeTextInformation [ ] ;
139
+ myInProgress ?: RealTimeTextInformation ;
140
+ } ;
101
141
/**
102
142
* Flag to indicate if captions are on
103
143
*/
@@ -137,12 +177,12 @@ export interface CaptionsBannerProps {
137
177
/**
138
178
* Optional callback to send real time text.
139
179
*/
140
- onSendRealTimeText ?: ( text : string , finalized ? : boolean ) => Promise < void > ;
180
+ onSendRealTimeText ?: ( text : string , finalized : boolean ) => Promise < void > ;
141
181
/* @conditional -compile-remove(rtt) */
142
182
/**
143
183
* Latest local real time text
144
184
*/
145
- latestLocalRealTimeText ?: CaptionsInformation ;
185
+ latestLocalRealTimeText ?: RealTimeTextInformation ;
146
186
}
147
187
148
188
const SCROLL_OFFSET_ALLOWANCE = 20 ;
@@ -154,6 +194,8 @@ const SCROLL_OFFSET_ALLOWANCE = 20;
154
194
export const CaptionsBanner = ( props : CaptionsBannerProps ) : JSX . Element => {
155
195
const {
156
196
captions,
197
+ /* @conditional -compile-remove(rtt) */
198
+ realTimeTexts,
157
199
isCaptionsOn,
158
200
startCaptionsInProgress,
159
201
onRenderAvatar,
@@ -171,6 +213,20 @@ export const CaptionsBanner = (props: CaptionsBannerProps): JSX.Element => {
171
213
const captionsScrollDivRef = useRef < HTMLUListElement > ( null ) ;
172
214
const [ isAtBottomOfScroll , setIsAtBottomOfScroll ] = useState < boolean > ( true ) ;
173
215
const theme = useTheme ( ) ;
216
+ /* @conditional -compile-remove(rtt) */
217
+ // merge realtimetexts and captions into one array based on timestamp
218
+ // Combine captions and realTimeTexts into one list
219
+ const combinedList : ( CaptionsInformation | RealTimeTextInformation ) [ ] = useMemo ( ( ) => {
220
+ return sortCaptionsAndRealTimeTexts ( captions , realTimeTexts ?. completedMessages ?? [ ] ) ;
221
+ } , [ captions , realTimeTexts ?. completedMessages ] ) ;
222
+
223
+ /* @conditional -compile-remove(rtt) */
224
+ const mergedCaptions : ( CaptionsInformation | RealTimeTextInformation ) [ ] = useMemo ( ( ) => {
225
+ return [ ...combinedList , ...( realTimeTexts ?. currentInProgress ?? [ ] ) , realTimeTexts ?. myInProgress ] as (
226
+ | CaptionsInformation
227
+ | RealTimeTextInformation
228
+ ) [ ] ;
229
+ } , [ combinedList , realTimeTexts ] ) ;
174
230
175
231
const scrollToBottom = ( ) : void => {
176
232
if ( captionsScrollDivRef . current ) {
@@ -209,7 +265,7 @@ export const CaptionsBanner = (props: CaptionsBannerProps): JSX.Element => {
209
265
/* @conditional -compile-remove(rtt) */
210
266
useEffect ( ( ) => {
211
267
// if the latest real time text sent by myself is final, clear the text field
212
- if ( latestLocalRealTimeText && ! latestLocalRealTimeText . isPartial ) {
268
+ if ( latestLocalRealTimeText && ! latestLocalRealTimeText . isTyping ) {
213
269
setTextFieldValue ( '' ) ;
214
270
}
215
271
} , [ latestLocalRealTimeText ] ) ;
@@ -232,13 +288,52 @@ export const CaptionsBanner = (props: CaptionsBannerProps): JSX.Element => {
232
288
bannerLinkLabel : strings . realTimeTextBannerLinkLabel ?? ''
233
289
} ;
234
290
291
+ const captionsTrampoline = ( ) : JSX . Element => {
292
+ /* @conditional -compile-remove(rtt) */
293
+ return (
294
+ < >
295
+ { mergedCaptions . map ( ( caption ) => {
296
+ if ( caption ) {
297
+ if ( 'message' in caption ) {
298
+ return (
299
+ < li key = { `RealTimeText - ${ caption . id } ` } className = { captionContainerClassName } data-is-focusable = { true } >
300
+ < RealTimeText { ...( caption as RealTimeTextInformation ) } />
301
+ </ li >
302
+ ) ;
303
+ }
304
+ return (
305
+ < li key = { `Captions - ${ caption . id } ` } className = { captionContainerClassName } data-is-focusable = { true } >
306
+ < _Caption { ...( caption as CaptionsInformation ) } onRenderAvatar = { onRenderAvatar } />
307
+ </ li >
308
+ ) ;
309
+ }
310
+ return < > </ > ;
311
+ } ) }
312
+ </ >
313
+ ) ;
314
+
315
+ return (
316
+ < >
317
+ { captions . map ( ( caption ) => {
318
+ return (
319
+ < li key = { caption . id } className = { captionContainerClassName } data-is-focusable = { true } >
320
+ < _Caption { ...( caption as CaptionsInformation ) } onRenderAvatar = { onRenderAvatar } />
321
+ </ li >
322
+ ) ;
323
+ } ) }
324
+ </ >
325
+ ) ;
326
+ } ;
327
+
235
328
return (
236
329
< >
237
330
{ ( startCaptionsInProgress || /* @conditional -compile-remove(rtt) */ isRealTimeTextOn ) && (
238
331
< FocusZone shouldFocusOnMount className = { captionsContainerClassName } data-ui-id = "captions-banner" >
239
332
{
240
333
/* @conditional -compile-remove(rtt) */ isRealTimeTextOn && (
241
- < _RTTDisclosureBanner strings = { realTimeTextDisclosureBannerStrings } />
334
+ < div style = { { paddingTop : '0.5rem' } } >
335
+ < _RTTDisclosureBanner strings = { realTimeTextDisclosureBannerStrings } />
336
+ </ div >
242
337
)
243
338
}
244
339
{ ( isCaptionsOn || /* @conditional -compile-remove(rtt) */ isRealTimeTextOn ) && (
@@ -251,21 +346,7 @@ export const CaptionsBanner = (props: CaptionsBannerProps): JSX.Element => {
251
346
}
252
347
data-ui-id = "captions-banner-inner"
253
348
>
254
- { captions . map ( ( caption ) => {
255
- /* @conditional -compile-remove(rtt) */
256
- if ( caption . isRealTimeText ) {
257
- return (
258
- < li key = { caption . id } className = { captionContainerClassName } data-is-focusable = { true } >
259
- < RealTimeText { ...caption } isTyping = { caption . isPartial } onRenderAvatar = { onRenderAvatar } />
260
- </ li >
261
- ) ;
262
- }
263
- return (
264
- < li key = { caption . id } className = { captionContainerClassName } data-is-focusable = { true } >
265
- < _Caption { ...caption } onRenderAvatar = { onRenderAvatar } />
266
- </ li >
267
- ) ;
268
- } ) }
349
+ { captionsTrampoline ( ) }
269
350
</ ul >
270
351
) }
271
352
{
@@ -276,7 +357,7 @@ export const CaptionsBanner = (props: CaptionsBannerProps): JSX.Element => {
276
357
onKeyDown = { handleKeyDown }
277
358
onChange = { ( _ , newValue ) => {
278
359
setTextFieldValue ( newValue || '' ) ;
279
- onSendRealTimeText ( newValue || '' ) ;
360
+ onSendRealTimeText ( newValue || '' , false ) ;
280
361
} }
281
362
/>
282
363
)
0 commit comments