diff --git a/src/thread.ts b/src/thread.ts index f8f39beef..24bbe072b 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -354,17 +354,15 @@ export class Thread { sortedArray: replies, sortDirection: 'ascending', selectValueToCompare: (reply) => reply.created_at.getTime(), + selectKey: (reply) => reply.id, }); - const actualIndex = - replies[index]?.id === message.id ? index : replies[index - 1]?.id === message.id ? index - 1 : null; - - if (actualIndex === null) { + if (replies[index]?.id !== message.id) { return; } const updatedReplies = [...replies]; - updatedReplies.splice(actualIndex, 1); + updatedReplies.splice(index, 1); this.state.partialNext({ replies: updatedReplies, diff --git a/src/utils.ts b/src/utils.ts index ba63af8b6..acb78dad9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -311,14 +311,26 @@ export function formatMessage({ needle, sortedArray, + selectKey, selectValueToCompare = (e) => e, sortDirection = 'ascending', }: { needle: T; sortedArray: readonly T[]; /** - * In array of objects (like messages), pick a specific - * property to compare needle value to. + * In an array of objects (like messages), pick a unique property identifying + * an element. It will be used to find a direct match for the needle element + * in case compare values are not unique. + * + * @example + * ```ts + * selectKey: (message) => message.id + * ``` + */ + selectKey?: (arrayElement: T) => string; + /** + * In an array of objects (like messages), pick a specific + * property to compare the needle value to. * * @example * ```ts @@ -353,11 +365,9 @@ export const findIndexInSortedArray = ({ const comparableMiddle = selectValueToCompare(sortedArray[middle]); - // if (comparableNeedle === comparableMiddle) return middle; - if ( (sortDirection === 'ascending' && comparableNeedle < comparableMiddle) || - (sortDirection === 'descending' && comparableNeedle > comparableMiddle) + (sortDirection === 'descending' && comparableNeedle >= comparableMiddle) ) { right = middle - 1; } else { @@ -365,6 +375,23 @@ export const findIndexInSortedArray = ({ } } + // In case there are several array elements with the same comparable value, search around the insertion + // point to possibly find an element with the same key. If found, prefer it. + // This, for example, prevents duplication of messages with the same creation date. + if (selectKey) { + const needleKey = selectKey(needle); + const step = sortDirection === 'ascending' ? -1 : +1; + for ( + let i = left + step; + 0 <= i && i < sortedArray.length && selectValueToCompare(sortedArray[i]) === comparableNeedle; + i += step + ) { + if (selectKey(sortedArray[i]) === needleKey) { + return i; + } + } + } + return left; }; @@ -410,19 +437,18 @@ export function addToMessageList( sortDirection: 'ascending', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion selectValueToCompare: (m) => m[sortBy]!.getTime(), + selectKey: (m) => m.id, }); // message already exists and not filtered with timestampChanged, update and return - if (!timestampChanged && newMessage.id) { - if (newMessages[insertionIndex] && newMessage.id === newMessages[insertionIndex].id) { - newMessages[insertionIndex] = newMessage; - return newMessages; - } - - if (newMessages[insertionIndex - 1] && newMessage.id === newMessages[insertionIndex - 1].id) { - newMessages[insertionIndex - 1] = newMessage; - return newMessages; - } + if ( + !timestampChanged && + newMessage.id && + newMessages[insertionIndex] && + newMessage.id === newMessages[insertionIndex].id + ) { + newMessages[insertionIndex] = newMessage; + return newMessages; } // do not add updated or deleted messages to the list if they already exist or come with a timestamp change diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 04f107804..93be91c47 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { generateMsg } from './test-utils/generateMessage'; -import { addToMessageList, formatMessage } from '../../src/utils'; +import { addToMessageList, findIndexInSortedArray, formatMessage } from '../../src/utils'; import type { FormatMessageResponse, MessageResponse } from '../../src'; @@ -105,3 +105,147 @@ describe('addToMessageList', () => { expect(messagesAfter[4]).to.equal(newMessage); }); }); + +describe('findIndexInSortedArray', () => { + it('finds index in the middle of haystack (asc)', () => { + const needle = 5; + const haystack = [1, 2, 3, 4, 6, 7, 8, 9]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + expect(index).to.eq(4); + }); + + it('finds index at the top of haystack (asc)', () => { + const needle = 0; + const haystack = [1, 2, 3, 4, 6, 7, 8, 9]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + expect(index).to.eq(0); + }); + + it('finds index at the bottom of haystack (asc)', () => { + const needle = 10; + const haystack = [1, 2, 3, 4, 6, 7, 8, 9]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + expect(index).to.eq(8); + }); + + it('in a haystack with duplicates, prefers index closer to the bottom (asc)', () => { + const needle = 5; + const haystack = [1, 5, 5, 5, 5, 5, 8, 9]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'ascending' }); + expect(index).to.eq(6); + }); + + it('in a haystack with duplicates, look up an item by key (asc)', () => { + const haystack: [key: string, value: number][] = [ + ['one', 1], + ['five-1', 5], + ['five-2', 5], + ['five-3', 5], + ['nine', 9], + ]; + + const selectKey = (tuple: [key: string, value: number]) => tuple[0]; + const selectValue = (tuple: [key: string, value: number]) => tuple[1]; + + expect( + findIndexInSortedArray({ + needle: ['five-1', 5], + sortedArray: haystack, + sortDirection: 'ascending', + selectKey, + selectValueToCompare: selectValue, + }), + ).to.eq(1); + + expect( + findIndexInSortedArray({ + needle: ['five-2', 5], + sortedArray: haystack, + sortDirection: 'ascending', + selectKey, + selectValueToCompare: selectValue, + }), + ).to.eq(2); + + expect( + findIndexInSortedArray({ + needle: ['five-3', 5], + sortedArray: haystack, + sortDirection: 'ascending', + selectKey, + selectValueToCompare: selectValue, + }), + ).to.eq(3); + }); + + it('finds index in the middle of haystack (desc)', () => { + const needle = 5; + const haystack = [9, 8, 7, 6, 4, 3, 2, 1]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + expect(index).to.eq(4); + }); + + it('finds index at the top of haystack (desc)', () => { + const needle = 10; + const haystack = [9, 8, 7, 6, 4, 3, 2, 1]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + expect(index).to.eq(0); + }); + + it('finds index at the bottom of haystack (desc)', () => { + const needle = 0; + const haystack = [9, 8, 7, 6, 4, 3, 2, 1]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + expect(index).to.eq(8); + }); + + it('in a haystack with duplicates, prefers index closer to the top (desc)', () => { + const needle = 5; + const haystack = [9, 8, 5, 5, 5, 5, 5, 1]; + const index = findIndexInSortedArray({ needle, sortedArray: haystack, sortDirection: 'descending' }); + expect(index).to.eq(2); + }); + + it('in a haystack with duplicates, look up an item by key (desc)', () => { + const haystack: [key: string, value: number][] = [ + ['nine', 9], + ['five-1', 5], + ['five-2', 5], + ['five-3', 5], + ['one', 1], + ]; + + const selectKey = (tuple: [key: string, value: number]) => tuple[0]; + const selectValue = (tuple: [key: string, value: number]) => tuple[1]; + + expect( + findIndexInSortedArray({ + needle: ['five-1', 5], + sortedArray: haystack, + sortDirection: 'descending', + selectKey, + selectValueToCompare: selectValue, + }), + ).to.eq(1); + + expect( + findIndexInSortedArray({ + needle: ['five-2', 5], + sortedArray: haystack, + sortDirection: 'descending', + selectKey, + selectValueToCompare: selectValue, + }), + ).to.eq(2); + + expect( + findIndexInSortedArray({ + needle: ['five-3', 5], + sortedArray: haystack, + sortDirection: 'descending', + selectKey, + selectValueToCompare: selectValue, + }), + ).to.eq(3); + }); +});