@@ -129,8 +129,6 @@ function upsertActivityWithSort(
129129
130130 const { channelData : { clientActivityID : incomingClientActivityID } = { } } = incomingActivity ;
131131
132- let incomingPosition : number | undefined = incomingActivity . channelData [ 'webchat:internal:position' ] ;
133-
134132 const nextActivities = activities . filter (
135133 ( { channelData : { clientActivityID } = { } , id } ) =>
136134 // We will remove all "sending messages" activities and activities with same ID
@@ -139,102 +137,93 @@ function upsertActivityWithSort(
139137 ! ( id && id === incomingActivity . id )
140138 ) ;
141139
142- // If `channelData['webchat:internal:position']` is already set, use that to find where to insert the activity.
143- // For example, when receiving the echoback, we will want to keep the existing position.
144- if ( typeof incomingPosition !== 'number' ) {
145- const incomingEntityPosition = incomingActivity . channelData ?. [ 'webchat:entity-position' ] ;
146- const incomingPartOf = incomingActivity . channelData ?. [ 'webchat:entity-part-of' ] ;
147- const incomingSequenceId = getSequenceIdOrDeriveFromTimestamp ( incomingActivity , ponyfill ) ;
148-
149- // TODO: [P0] Turn (activity) => boolean into comparer (x, y) => number.
150- // It is not trivial to write in current form.
151- // We can use comparer for insertion sort too, so let's rewrite in comparer form.
152- let indexToInsert = nextActivities . findIndex ( activity => {
153- // TODO: [P1] #3953 We should move this patching logic to a DLJS wrapper for simplicity.
154- // If the message does not have sequence ID, use these fallback values:
155- // 1. `entities.position` where `entities.isPartOf[@type === 'HowTo']`
156- // - If they are not of same set, ignore `entities.position`
157- // 2. `channelData.streamSequence` field for same session IDk
158- // 3. `channelData['webchat:sequence-id']`
159- // - If not available, it will fallback to `+new Date(timestamp)`
160- // - Outgoing activity will not have `timestamp` field
161- const { channelData = { } } = activity ;
162- const currentPosition = channelData [ 'webchat:entity-position' ] ;
163- const currentPartOf = channelData [ 'webchat:entity-part-of' ] ;
164-
165- const bothHavePosition = typeof currentPosition === 'number' && typeof incomingEntityPosition === 'number' ;
166- const bothArePartOf = typeof currentPartOf === 'string' && currentPartOf === incomingPartOf ;
167-
168- // For activities in the same creative work part, position is primary sort key
169- if ( bothHavePosition && bothArePartOf ) {
170- return currentPosition > incomingEntityPosition ;
171- }
140+ const incomingEntityPosition = incomingActivity . channelData ?. [ 'webchat:entity-position' ] ;
141+ const incomingPartOf = incomingActivity . channelData ?. [ 'webchat:entity-part-of' ] ;
142+ const incomingSequenceId = getSequenceIdOrDeriveFromTimestamp ( incomingActivity , ponyfill ) ;
143+
144+ // TODO: [P0] Turn (activity) => boolean into comparer (x, y) => number.
145+ // It is not trivial to write in current form.
146+ // We can use comparer for insertion sort too, so let's rewrite in comparer form.
147+ let indexToInsert = nextActivities . findIndex ( activity => {
148+ // TODO: [P1] #3953 We should move this patching logic to a DLJS wrapper for simplicity.
149+ // If the message does not have sequence ID, use these fallback values:
150+ // 1. `entities.position` where `entities.isPartOf[@type === 'HowTo']`
151+ // - If they are not of same set, ignore `entities.position`
152+ // 2. `channelData.streamSequence` field for same session IDk
153+ // 3. `channelData['webchat:sequence-id']`
154+ // - If not available, it will fallback to `+new Date(timestamp)`
155+ // - Outgoing activity will not have `timestamp` field
156+ const { channelData = { } } = activity ;
157+ const currentEntityPosition = channelData [ 'webchat:entity-position' ] ;
158+ const currentEntityPartOf = channelData [ 'webchat:entity-part-of' ] ;
159+
160+ const bothHavePosition = typeof currentEntityPosition === 'number' && typeof incomingEntityPosition === 'number' ;
161+ const bothArePartOf = typeof currentEntityPartOf === 'string' && currentEntityPartOf === incomingPartOf ;
162+
163+ // For activities in the same creative work part, position is primary sort key
164+ if ( bothHavePosition && bothArePartOf ) {
165+ return currentEntityPosition > incomingEntityPosition ;
166+ }
172167
173- const currentLivestreamingMetadata = getActivityLivestreamingMetadata ( activity ) ;
168+ const currentLivestreamingMetadata = getActivityLivestreamingMetadata ( activity ) ;
174169
175- if (
176- incomingLivestreamingMetadata &&
177- currentLivestreamingMetadata &&
178- incomingLivestreamingMetadata . sessionId === currentLivestreamingMetadata . sessionId
179- ) {
180- return currentLivestreamingMetadata . sequenceNumber > incomingLivestreamingMetadata . sequenceNumber ;
181- }
170+ if (
171+ incomingLivestreamingMetadata &&
172+ currentLivestreamingMetadata &&
173+ incomingLivestreamingMetadata . sessionId === currentLivestreamingMetadata . sessionId
174+ ) {
175+ return currentLivestreamingMetadata . sequenceNumber > incomingLivestreamingMetadata . sequenceNumber ;
176+ }
182177
183- const currentSequenceId = getSequenceIdOrDeriveFromTimestamp ( activity , ponyfill ) ;
178+ const currentSequenceId = getSequenceIdOrDeriveFromTimestamp ( activity , ponyfill ) ;
184179
185- if ( typeof incomingSequenceId === 'number' ) {
186- if ( typeof currentSequenceId === 'number' ) {
187- return currentSequenceId > incomingSequenceId ;
188- }
189-
190- // Always insert activity whose has sequence ID before those whose doesn't have sequence ID.
191- return true ;
192- } else if ( typeof currentSequenceId === 'number' ) {
193- return false ;
180+ if ( typeof incomingSequenceId === 'number' ) {
181+ if ( typeof currentSequenceId === 'number' ) {
182+ return currentSequenceId > incomingSequenceId ;
194183 }
195184
196- // No more properties can be used to find a good insertion spot.
197- // Return `false` so the activity will append to the end.
185+ // Always insert activity whose has sequence ID before those whose doesn't have sequence ID.
186+ return true ;
187+ } else if ( typeof currentSequenceId === 'number' ) {
198188 return false ;
199- } ) ;
200-
201- if ( ! ~ indexToInsert ) {
202- // If no right place can be found, append it.
203- indexToInsert = nextActivities . length ;
204189 }
205190
206- const prevActivity : WebChatActivity = nextActivities . at ( indexToInsert - 1 ) ;
207- const nextActivity : WebChatActivity = nextActivities . at ( indexToInsert ) ;
191+ // No more properties can be used to find a good insertion spot.
192+ // Return `false` so the activity will append to the end.
193+ return false ;
194+ } ) ;
208195
209- if ( prevActivity ) {
210- const prevPosition = prevActivity . channelData [ 'webchat:internal:position' ] ;
196+ if ( ! ~ indexToInsert ) {
197+ // If no right place can be found, append it.
198+ indexToInsert = nextActivities . length ;
199+ }
211200
212- if ( nextActivity ) {
213- const nextSequenceId = nextActivity . channelData [ 'webchat:internal:position' ] ;
201+ const prevActivity : WebChatActivity = indexToInsert === 0 ? undefined : nextActivities . at ( indexToInsert - 1 ) ;
202+ const nextActivity : WebChatActivity = nextActivities . at ( indexToInsert ) ;
203+ let incomingPosition : number ;
214204
215- // eslint-disable-next-line no-magic-numbers
216- incomingPosition = ( prevPosition + nextSequenceId ) / 2 ;
217- } else {
218- incomingPosition = prevPosition + 1 ;
219- }
220- } else if ( nextActivity ) {
205+ if ( prevActivity ) {
206+ const prevPosition = prevActivity . channelData [ 'webchat:internal:position' ] ;
207+
208+ if ( nextActivity ) {
221209 const nextSequenceId = nextActivity . channelData [ 'webchat:internal:position' ] ;
222210
223- incomingPosition = nextSequenceId - 1 ;
211+ // eslint-disable-next-line no-magic-numbers
212+ incomingPosition = ( prevPosition + nextSequenceId ) / 2 ;
224213 } else {
225- incomingPosition = 0 ;
214+ incomingPosition = prevPosition + 1 ;
226215 }
216+ } else if ( nextActivity ) {
217+ const nextSequenceId = nextActivity . channelData [ 'webchat:internal:position' ] ;
218+
219+ incomingPosition = nextSequenceId - 1 ;
220+ } else {
221+ incomingPosition = 1 ;
227222 }
228223
229- const indexToInsert = nextActivities . findIndex (
230- activity => activity . channelData [ 'webchat:internal:position' ] > incomingPosition
231- ) ;
224+ incomingActivity = updateIn ( incomingActivity , [ 'channelData' , 'webchat:internal:position' ] , ( ) => incomingPosition ) ;
232225
233- nextActivities . splice (
234- ~ indexToInsert ? indexToInsert : nextActivities . length ,
235- 0 ,
236- updateIn ( incomingActivity , [ 'channelData' , 'webchat:internal:position' ] , ( ) => incomingPosition )
237- ) ;
226+ nextActivities . splice ( indexToInsert , 0 , incomingActivity ) ;
238227
239228 return nextActivities ;
240229}
@@ -273,6 +262,16 @@ export default function createActivitiesReducer(
273262 activity = updateIn ( activity , [ 'channelData' , 'state' ] , ( ) => SENDING ) ;
274263 activity = updateIn ( activity , [ 'channelData' , 'webchat:send-status' ] , ( ) => SENDING ) ;
275264
265+ // Assume the message was sent immediately after the very last message.
266+ // This helps to maintain the order of the outgoing message before the server respond.
267+ activity = updateIn ( activity , [ 'timestamp' ] , ( ) => {
268+ const lastTimestamp = state . at ( - 1 ) ?. timestamp ;
269+
270+ return (
271+ lastTimestamp ? new ponyfill . Date ( + new ponyfill . Date ( lastTimestamp ) + 1 ) : new ponyfill . Date ( 1 )
272+ ) . toISOString ( ) ;
273+ } ) ;
274+
276275 state = upsertActivityWithSort ( state , activity , ponyfill ) ;
277276 }
278277
@@ -394,11 +393,6 @@ export default function createActivitiesReducer(
394393 } = existingActivity ;
395394
396395 activity = updateIn ( activity , [ 'channelData' , 'webchat:internal:id' ] , ( ) => permanentId ) ;
397- activity = updateIn (
398- activity ,
399- [ 'channelData' , 'webchat:internal:position' ] ,
400- ( ) => existingActivity . channelData [ 'webchat:internal:position' ]
401- ) ;
402396
403397 if ( sendStatus === SENDING || sendStatus === SEND_FAILED || sendStatus === SENT ) {
404398 activity = updateIn ( activity , [ 'channelData' , 'webchat:send-status' ] , ( ) => sendStatus ) ;
0 commit comments