diff --git a/src/sidebar/components/ThreadList.tsx b/src/sidebar/components/ThreadList.tsx index a99aff1d204..0e02a238440 100644 --- a/src/sidebar/components/ThreadList.tsx +++ b/src/sidebar/components/ThreadList.tsx @@ -187,6 +187,7 @@ export default function ThreadList({ threads }: ThreadListProps) { const store = useSidebarStore(); const editing = store.countDrafts() > 0; const highlightedAnnotations = store.highlightedAnnotations(); + const allAnnotations = store.allAnnotations(); // Get the `$tag` of the most recently created unsaved annotation. const newAnnotationTag = (() => { @@ -211,20 +212,32 @@ export default function ThreadList({ threads }: ThreadListProps) { } }, [store, newAnnotationTag]); - const mostRecentlyHighlightedAnnotationId = useMemo( - // If multiple highlighted annotations exist, assume that the last one in - // the list is the most recent. - () => highlightedAnnotations[highlightedAnnotations.length - 1], - [highlightedAnnotations], - ); + const mostRecentHighlightedAnnotationId = useMemo(() => { + const highlightedAnnos = allAnnotations.filter( + anno => anno.id && highlightedAnnotations.includes(anno.id), + ); + // Get the annotation with most recent updated field, which contains a + // date in ISO format. This means their alphabetical and chronological + // orders match. + const mostRecentHighlightedAnnotation = + highlightedAnnos.reduce( + (mostRecent, current) => + !mostRecent || mostRecent.updated < current.updated + ? current + : mostRecent, + null, + ); + + return mostRecentHighlightedAnnotation?.id; + }, [allAnnotations, highlightedAnnotations]); // Scroll to the most recently highlighted annotation, unless creating/editing // another annotation useEffect(() => { - if (!editing && mostRecentlyHighlightedAnnotationId) { - setScrollToId(mostRecentlyHighlightedAnnotationId); + if (!editing && mostRecentHighlightedAnnotationId) { + setScrollToId(mostRecentHighlightedAnnotationId); } - }, [editing, mostRecentlyHighlightedAnnotationId]); + }, [editing, mostRecentHighlightedAnnotationId]); // Effect to scroll a particular thread into view. This is mainly used to // scroll a newly created annotation into view. diff --git a/src/sidebar/components/test/ThreadList-test.js b/src/sidebar/components/test/ThreadList-test.js index 69710e971e5..1d80c42a9bf 100644 --- a/src/sidebar/components/test/ThreadList-test.js +++ b/src/sidebar/components/test/ThreadList-test.js @@ -57,6 +57,7 @@ describe('ThreadList', () => { unsavedAnnotations: sinon.stub().returns([]), countDrafts: sinon.stub().returns(0), highlightedAnnotations: sinon.stub().returns([]), + allAnnotations: sinon.stub().returns([]), }; fakeTopThread = { @@ -193,13 +194,65 @@ describe('ThreadList', () => { assert.notCalled(fakeScrollTop); }); - it('should set the scroll container `scrollTop` to first highlighted annotation', () => { - fakeStore.highlightedAnnotations.returns(['t2', 't3']); - createComponent(); + [ + { + annotationUpdates: { + t2: '2024-01-01T10:40:00', + t3: '2024-01-01T10:41:00', // Most recent + t4: '2024-01-01T10:39:00', + }, + // The most recent highlighted annotation is the third. + // At default height (200) should be at 400px. + expectedScrollTop: 400, + }, + { + annotationUpdates: { + t1: '2024-01-01T10:42:00', // Most recent + t3: '2024-01-01T10:39:00', + t4: '2024-01-01T10:39:00', + }, + // The most recent highlighted annotation is the first. + // At default height (200) should be at 0px. + expectedScrollTop: 0, + }, + { + annotationUpdates: { + t1: '2024-01-01T10:42:00', + t3: '2024-01-01T10:39:00', + t4: '2024-01-01T10:51:00', // Most recent + }, + // The most recent highlighted annotation is the fourth. + // At default height (200) should be at 600px. + expectedScrollTop: 600, + }, + { + annotationUpdates: { + t1: '2024-01-01T10:42:00', + t3: '2024-01-01T10:39:00', + t2: '2024-01-01T10:51:00', // Most recent + }, + // The most recent highlighted annotation is the second. + // At default height (200) should be at 200px. + expectedScrollTop: 200, + }, + ].forEach(({ annotationUpdates, expectedScrollTop }) => { + it('should set the scroll container `scrollTop` to most recent highlighted annotation', () => { + fakeStore.highlightedAnnotations.returns( + Object.keys(annotationUpdates), + ); + fakeStore.allAnnotations.returns([ + {}, // Discarded + { id: 't1', updated: annotationUpdates.t1 }, + { id: 't2', updated: annotationUpdates.t2 }, + { id: 't3', updated: annotationUpdates.t3 }, + { id: 't4', updated: annotationUpdates.t4 }, + { id: 't5', updated: annotationUpdates.t5 }, + ]); + + createComponent(); - // The last highlighted annotation is the third in the collection of - // threads. At default height (200) should be at 400px. - assert.calledWith(fakeScrollTop, 400); + assert.calledWith(fakeScrollTop, expectedScrollTop); + }); }); });