11import { getActivityLivestreamingMetadata , type WebChatActivity } from 'botframework-webchat-core' ;
2+ import { iteratorFind } from 'iter-fest' ;
23import React , { memo , useCallback , useMemo , type ReactNode } from 'react' ;
34
45import numberWithInfinity from '../../hooks/private/numberWithInfinity' ;
@@ -7,49 +8,155 @@ import ActivityTypingContext, { ActivityTypingContextType } from './private/Cont
78import useReduceActivities from './private/useReduceActivities' ;
89import { type AllTyping } from './types/AllTyping' ;
910
11+ type Entry = {
12+ livestreamActivities : Map <
13+ string ,
14+ {
15+ activity : WebChatActivity ;
16+ contentful : boolean ;
17+ firstReceivedAt : number ;
18+ lastReceivedAt : number ;
19+ }
20+ > ;
21+ name : string | undefined ;
22+ role : 'bot' | 'user' ;
23+ typingIndicator :
24+ | {
25+ activity : WebChatActivity ;
26+ duration : number ;
27+ firstReceivedAt : number ;
28+ lastReceivedAt : number ;
29+ }
30+ | undefined ;
31+ } ;
32+
1033type Props = Readonly < { children ?: ReactNode | undefined } > ;
1134
1235const ActivityTypingComposer = ( { children } : Props ) => {
1336 const [ { Date } ] = usePonyfill ( ) ;
1437
1538 const reducer = useCallback (
1639 (
17- prevTypingState : ReadonlyMap < string , AllTyping > | undefined ,
40+ prevTypingState : ReadonlyMap < string , Readonly < Entry > > | undefined ,
1841 activity : WebChatActivity
19- ) : ReadonlyMap < string , AllTyping > | undefined => {
42+ ) : ReadonlyMap < string , Readonly < Entry > > | undefined => {
2043 const {
21- from,
22- from : { id, role } ,
44+ from : { id, name, role } ,
2345 type
2446 } = activity ;
2547
48+ if ( role === 'channel' ) {
49+ return prevTypingState ;
50+ }
51+
52+ // A normal message activity, or final activity (which could be "message" or "typing"), will remove the typing indicator.
53+ const receivedAt = activity . channelData . webChat . receivedAt || Date . now ( ) ;
54+
2655 const livestreamingMetadata = getActivityLivestreamingMetadata ( activity ) ;
2756 const typingState = new Map ( prevTypingState ) ;
57+ const existingEntry = typingState . get ( id ) ;
58+ const mutableEntry : Entry = {
59+ typingIndicator : undefined ,
60+ ...existingEntry ,
61+ livestreamActivities : new Map ( existingEntry ?. livestreamActivities ) ,
62+ name,
63+ role
64+ } ;
65+
66+ if ( livestreamingMetadata ) {
67+ mutableEntry . typingIndicator = undefined ;
68+
69+ const { sessionId } = livestreamingMetadata ;
2870
29- if ( type === 'message' || livestreamingMetadata ?. type === 'final activity' ) {
30- // A normal message activity, or final activity (which could be "message" or "typing"), will remove the typing indicator.
31- typingState . delete ( id ) ;
32- } else if ( type === 'typing' && ( role === 'bot' || role === 'user' ) ) {
33- const currentTyping = typingState . get ( id ) ;
34- // TODO: When we rework on types of DLActivity, we will make sure all activities has "webChat.receivedAt", this coalesces can be removed.
35- const receivedAt = activity . channelData . webChat ?. receivedAt || Date . now ( ) ;
36-
37- typingState . set ( id , {
38- firstReceivedAt : currentTyping ?. firstReceivedAt || receivedAt ,
39- lastActivityDuration : numberWithInfinity ( activity . channelData . webChat ?. styleOptions ?. typingAnimationDuration ) ,
40- lastReceivedAt : receivedAt ,
41- name : from . name ,
42- role,
43- type : livestreamingMetadata && livestreamingMetadata . type !== 'indicator only' ? 'livestream' : 'busy'
71+ if ( livestreamingMetadata . type === 'final activity' ) {
72+ mutableEntry . livestreamActivities . delete ( sessionId ) ;
73+ } else {
74+ mutableEntry . livestreamActivities . set (
75+ sessionId ,
76+ Object . freeze ( {
77+ firstReceivedAt : Date . now ( ) ,
78+ ...mutableEntry . livestreamActivities . get ( sessionId ) ,
79+ activity,
80+ contentful : livestreamingMetadata . type !== 'indicator only' ,
81+ lastReceivedAt : receivedAt
82+ } )
83+ ) ;
84+ }
85+ } else if ( type === 'message' ) {
86+ mutableEntry . typingIndicator = undefined ;
87+ } else if ( type === 'typing' ) {
88+ mutableEntry . typingIndicator = Object . freeze ( {
89+ activity,
90+ duration : numberWithInfinity ( activity . channelData . webChat ?. styleOptions ?. typingAnimationDuration ) ,
91+ firstReceivedAt : mutableEntry . typingIndicator ?. firstReceivedAt || Date . now ( ) ,
92+ lastReceivedAt : receivedAt
4493 } ) ;
4594 }
4695
96+ typingState . set ( id , Object . freeze ( mutableEntry ) ) ;
97+
4798 return Object . freeze ( typingState ) ;
4899 } ,
49100 [ Date ]
50101 ) ;
51102
52- const allTyping : ReadonlyMap < string , AllTyping > = useReduceActivities ( reducer ) || Object . freeze ( new Map ( ) ) ;
103+ const state : ReadonlyMap < string , Entry > = useReduceActivities ( reducer ) || Object . freeze ( new Map ( ) ) ;
104+
105+ const allTyping = useMemo ( ( ) => {
106+ const map = new Map < string , AllTyping > ( ) ;
107+
108+ for ( const [ id , entry ] of state . entries ( ) ) {
109+ const firstContentfulLivestream = iteratorFind (
110+ entry . livestreamActivities . values ( ) ,
111+ ( { contentful } ) => contentful
112+ ) ;
113+
114+ const firstContentlessLivestream = iteratorFind (
115+ entry . livestreamActivities . values ( ) ,
116+ ( { contentful } ) => ! contentful
117+ ) ;
118+
119+ if ( firstContentfulLivestream ) {
120+ map . set (
121+ id ,
122+ Object . freeze ( {
123+ firstReceivedAt : firstContentfulLivestream . firstReceivedAt ,
124+ lastActivityDuration : Infinity ,
125+ lastReceivedAt : firstContentfulLivestream . lastReceivedAt ,
126+ name : entry . name ,
127+ role : entry . role ,
128+ type : 'livestream'
129+ } satisfies AllTyping )
130+ ) ;
131+ } else if ( firstContentlessLivestream ) {
132+ map . set (
133+ id ,
134+ Object . freeze ( {
135+ firstReceivedAt : firstContentlessLivestream . firstReceivedAt ,
136+ lastActivityDuration : Infinity ,
137+ lastReceivedAt : firstContentlessLivestream . lastReceivedAt ,
138+ name : entry . name ,
139+ role : entry . role ,
140+ type : 'busy'
141+ } satisfies AllTyping )
142+ ) ;
143+ } else if ( entry . typingIndicator ) {
144+ map . set (
145+ id ,
146+ Object . freeze ( {
147+ firstReceivedAt : entry . typingIndicator . firstReceivedAt ,
148+ lastActivityDuration : entry . typingIndicator . duration ,
149+ lastReceivedAt : entry . typingIndicator . lastReceivedAt ,
150+ name : entry . name ,
151+ role : entry . role ,
152+ type : 'busy'
153+ } satisfies AllTyping )
154+ ) ;
155+ }
156+ }
157+
158+ return map ;
159+ } , [ state ] ) ;
53160
54161 const allTypingState = useMemo ( ( ) => Object . freeze ( [ allTyping ] as const ) , [ allTyping ] ) ;
55162
0 commit comments