Skip to content

Commit da0e41d

Browse files
committed
Keep outgoing pending activity at same position
1 parent e324661 commit da0e41d

File tree

1 file changed

+78
-84
lines changed

1 file changed

+78
-84
lines changed

packages/core/src/reducers/createActivitiesReducer.ts

Lines changed: 78 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)