From 15e970708237e9d4cbdb31d98977139e354daeb3 Mon Sep 17 00:00:00 2001 From: Duncan Hsu Date: Wed, 24 Jun 2026 16:42:10 -0700 Subject: [PATCH 1/5] feat(activity-feed-v2): emit timestamped comment markers to video player --- .../content-sidebar/ActivitySidebar.js | 2 + src/elements/content-sidebar/SidebarPanels.js | 1 + .../activity-feed-v2/ActivityFeedV2.tsx | 54 +++++++++ .../__tests__/ActivityFeedV2.test.tsx | 108 ++++++++++++++++++ .../content-sidebar/activity-feed-v2/types.ts | 2 + 5 files changed, 167 insertions(+) diff --git a/src/elements/content-sidebar/ActivitySidebar.js b/src/elements/content-sidebar/ActivitySidebar.js index 7d03e9e1c4..f6f8ac6216 100644 --- a/src/elements/content-sidebar/ActivitySidebar.js +++ b/src/elements/content-sidebar/ActivitySidebar.js @@ -116,6 +116,7 @@ type ExternalProps = { type PropsWithoutContext = { elementId: string, file: BoxItem, + getViewer?: Function, hasSidebarInitialized?: boolean, isDisabled: boolean, onAnnotationSelect: Function, @@ -1425,6 +1426,7 @@ class ActivitySidebar extends React.PureComponent { createTask={this.createTask} currentUser={currentUser} feedItems={this.getFilteredFeedItems()} + getViewer={this.props.getViewer} file={file} getApproverWithQuery={this.getApprover} getAvatarUrl={this.getAvatarUrl} diff --git a/src/elements/content-sidebar/SidebarPanels.js b/src/elements/content-sidebar/SidebarPanels.js index 1852192a46..076ceb4373 100644 --- a/src/elements/content-sidebar/SidebarPanels.js +++ b/src/elements/content-sidebar/SidebarPanels.js @@ -435,6 +435,7 @@ class SidebarPanels extends React.Component { currentUser={currentUser} currentUserError={currentUserError} file={file} + getViewer={getViewer} hasSidebarInitialized={isInitialized} onAnnotationSelect={onAnnotationSelect} onVersionChange={onVersionChange} diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index ea7488cd49..ade6510534 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -44,6 +44,7 @@ const ActivityFeedV2 = ({ getAvatarUrl, getMentionAsync, getTaskCollaborators, + getViewer, hasTasks = true, isDisabled = false, isTimestampedCommentsEnabled = false, @@ -307,6 +308,59 @@ const ActivityFeedV2 = ({ ? { formattedTimestamp, isPressed: isTimestampPressed, onPressedChange } : undefined; + React.useEffect(() => { + if (!getViewer || !isVideo) return undefined; + const viewer = getViewer(); + if (!viewer) return undefined; + + const markers: Array<{ + avatarUrl?: string; + colorIndex?: number; + id: string; + initial?: string; + time: number; + type: 'annotation' | 'comment'; + }> = []; + for (const item of filteredItems) { + if (item.type === 'comment' && item.annotationTimestampMs != null) { + const author = item.messages[0]?.author; + markers.push({ + avatarUrl: author?.avatarUrl ?? undefined, + colorIndex: author?.id ?? 0, + id: item.id, + initial: author?.name?.[0] ?? undefined, + time: item.annotationTimestampMs / 1000, + type: 'comment', + }); + } else if (item.type === 'annotation') { + const loc = item.annotation?.target?.location; + if (loc?.type === 'frame' && loc.value != null) { + const author = item.messages[0]?.author; + markers.push({ + avatarUrl: author?.avatarUrl ?? undefined, + colorIndex: author?.id ?? 0, + id: item.id, + initial: author?.name?.[0] ?? undefined, + time: loc.value / 1000, + type: 'annotation', + }); + } + } + } + viewer.emit('commentmarkers', markers); + + const handleMarkerSelect = ({ id }: { id: string }) => { + const el = document.querySelector(`[data-activity-id="${CSS.escape(id)}"]`); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }; + viewer.addListener('commentmarkerselect', handleMarkerSelect); + return () => { + viewer.removeListener('commentmarkerselect', handleMarkerSelect); + }; + }, [filteredItems, getViewer, isVideo]); + const handleCommentPost = React.useCallback( async (content: unknown) => { if (!onCommentCreate) return; diff --git a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx index 9b77f8e5fd..e3e65cddb6 100644 --- a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx @@ -1046,4 +1046,112 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { expect(mockScrollTo).toHaveBeenLastCalledWith('new-comment'); }); }); + + describe('comment markers', () => { + const mockViewer = { + addListener: jest.fn(), + emit: jest.fn(), + removeListener: jest.fn(), + }; + const mockGetViewer = jest.fn(() => mockViewer); + + const timestampedComment = { + ...mockComment, + id: 'ts-comment-1', + tagged_message: '#[timestamp:5000,versionId:123] Hello', + }; + + const frameAnnotation = { + ...mockAnnotation, + id: 'frame-ann-1', + target: { location: { type: 'frame', value: 10000 }, type: 'region' }, + }; + + const renderComponentWithMarkers = (props: Partial = {}) => + render( + , + ); + + beforeEach(() => { + mockViewer.addListener.mockClear(); + mockViewer.emit.mockClear(); + mockViewer.removeListener.mockClear(); + mockGetViewer.mockClear(); + }); + + test('should not emit commentmarkers when getViewer is not provided', () => { + renderComponentWithMarkers({ getViewer: undefined }); + expect(mockViewer.emit).not.toHaveBeenCalled(); + }); + + test('should not emit commentmarkers when file is not a video', () => { + renderComponentWithMarkers({ file: { extension: 'pdf', file_version: { id: '1' } } }); + expect(mockViewer.emit).not.toHaveBeenCalledWith('commentmarkers', expect.anything()); + }); + + test('should not emit commentmarkers when getViewer returns null', () => { + renderComponentWithMarkers({ getViewer: jest.fn(() => null) }); + expect(mockViewer.emit).not.toHaveBeenCalled(); + }); + + test('should emit commentmarkers with timestamped comment data', () => { + renderComponentWithMarkers(); + expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', [ + expect.objectContaining({ + id: 'ts-comment-1', + time: 5, + type: 'comment', + }), + ]); + }); + + test('should emit commentmarkers with frame annotation data', () => { + renderComponentWithMarkers({ feedItems: [frameAnnotation] as ActivityFeedV2Props['feedItems'] }); + expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', [ + expect.objectContaining({ + id: 'frame-ann-1', + time: 10, + type: 'annotation', + }), + ]); + }); + + test('should include author avatar and initial in markers', () => { + renderComponentWithMarkers(); + expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', [ + expect.objectContaining({ + initial: 'C', + colorIndex: 2, + }), + ]); + }); + + test('should register commentmarkerselect listener on viewer', () => { + renderComponentWithMarkers(); + expect(mockViewer.addListener).toHaveBeenCalledWith('commentmarkerselect', expect.any(Function)); + }); + + test('should remove commentmarkerselect listener on unmount', () => { + const { unmount } = renderComponentWithMarkers(); + unmount(); + expect(mockViewer.removeListener).toHaveBeenCalledWith('commentmarkerselect', expect.any(Function)); + }); + + test('should not include non-timestamped comments in markers', () => { + renderComponentWithMarkers({ feedItems: [mockComment] as ActivityFeedV2Props['feedItems'] }); + expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', []); + }); + + test('should not include page-based annotations in markers', () => { + renderComponentWithMarkers({ feedItems: [mockAnnotation] as ActivityFeedV2Props['feedItems'] }); + expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', []); + }); + }); }); diff --git a/src/elements/content-sidebar/activity-feed-v2/types.ts b/src/elements/content-sidebar/activity-feed-v2/types.ts index e0d0984af6..ce8da222bd 100644 --- a/src/elements/content-sidebar/activity-feed-v2/types.ts +++ b/src/elements/content-sidebar/activity-feed-v2/types.ts @@ -94,6 +94,8 @@ export type ActivityFeedV2Props = { getAvatarUrl?: GetAvatarUrl; getMentionAsync?: (searchStr: string) => Promise>>; getTaskCollaborators?: (task: TaskNew) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getViewer?: () => any; hasTasks?: boolean; isDisabled?: boolean; isTimestampedCommentsEnabled?: boolean; From 6c3dfd2ad714e026263c4a0f35f9b7ab68f28e4e Mon Sep 17 00:00:00 2001 From: Duncan Hsu Date: Wed, 24 Jun 2026 17:02:02 -0700 Subject: [PATCH 2/5] fix(scroll): fix scroll to comment on marker click --- .../activity-feed-v2/ActivityFeedV2.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index ade6510534..9f736e6f16 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -350,10 +350,12 @@ const ActivityFeedV2 = ({ viewer.emit('commentmarkers', markers); const handleMarkerSelect = ({ id }: { id: string }) => { - const el = document.querySelector(`[data-activity-id="${CSS.escape(id)}"]`); - if (el) { - el.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } + requestAnimationFrame(() => { + const el = document.querySelector(`[data-activity-id="${CSS.escape(id)}"]`); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); }; viewer.addListener('commentmarkerselect', handleMarkerSelect); return () => { From cab312049fd33579c16a04ddbed8c27db5590d13 Mon Sep 17 00:00:00 2001 From: Duncan Hsu Date: Thu, 25 Jun 2026 11:09:36 -0700 Subject: [PATCH 3/5] refactor(activity-feed-v2): address PR comments --- .../activity-feed-v2/ActivityFeedV2.tsx | 32 ++++++++++++------- .../__tests__/ActivityFeedV2.test.tsx | 2 ++ .../content-sidebar/activity-feed-v2/types.ts | 9 ++++-- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index 9f736e6f16..a42b364c50 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -72,6 +72,8 @@ const ActivityFeedV2 = ({ }: ActivityFeedV2Props) => { const intl = useIntl(); const scrollHandle = useActivityFeedScroll(); + const scrollHandleRef = React.useRef(scrollHandle); + scrollHandleRef.current = scrollHandle; const currentUserId = currentUser?.id; const headerTitle = intl.formatMessage(commonMessages.sidebarActivityTitle); @@ -308,11 +310,8 @@ const ActivityFeedV2 = ({ ? { formattedTimestamp, isPressed: isTimestampPressed, onPressedChange } : undefined; - React.useEffect(() => { - if (!getViewer || !isVideo) return undefined; - const viewer = getViewer(); - if (!viewer) return undefined; - + const commentMarkers = React.useMemo(() => { + if (!isVideo) return []; const markers: Array<{ avatarUrl?: string; colorIndex?: number; @@ -347,21 +346,32 @@ const ActivityFeedV2 = ({ } } } - viewer.emit('commentmarkers', markers); + return markers; + }, [filteredItems, isVideo]); + + React.useEffect(() => { + if (!getViewer || !isVideo) return; + const viewer = getViewer(); + if (!viewer) return; + viewer.emit('commentmarkers', commentMarkers); + }, [commentMarkers, getViewer, isVideo]); + + React.useEffect(() => { + if (!getViewer || !isVideo) return undefined; + const viewer = getViewer(); + if (!viewer) return undefined; const handleMarkerSelect = ({ id }: { id: string }) => { requestAnimationFrame(() => { - const el = document.querySelector(`[data-activity-id="${CSS.escape(id)}"]`); - if (el) { - el.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } + scrollHandleRef.current?.scrollTo(id); }); }; viewer.addListener('commentmarkerselect', handleMarkerSelect); return () => { + viewer.emit('commentmarkers', []); viewer.removeListener('commentmarkerselect', handleMarkerSelect); }; - }, [filteredItems, getViewer, isVideo]); + }, [getViewer, isVideo]); const handleCommentPost = React.useCallback( async (content: unknown) => { diff --git a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx index e3e65cddb6..4b2df4bee1 100644 --- a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx @@ -1140,7 +1140,9 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { test('should remove commentmarkerselect listener on unmount', () => { const { unmount } = renderComponentWithMarkers(); + mockViewer.emit.mockClear(); unmount(); + expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', []); expect(mockViewer.removeListener).toHaveBeenCalledWith('commentmarkerselect', expect.any(Function)); }); diff --git a/src/elements/content-sidebar/activity-feed-v2/types.ts b/src/elements/content-sidebar/activity-feed-v2/types.ts index ce8da222bd..bba219cd8b 100644 --- a/src/elements/content-sidebar/activity-feed-v2/types.ts +++ b/src/elements/content-sidebar/activity-feed-v2/types.ts @@ -83,6 +83,12 @@ export type ActivityFeedV2File = { file_version?: { id: string }; }; +export type ViewerHandle = { + addListener: (event: string, handler: (payload: unknown) => void) => void; + emit: (event: string, payload: unknown) => void; + removeListener: (event: string, handler: (payload: unknown) => void) => void; +}; + export type ActivityFeedV2Props = { activeFeedEntryId?: string; approverSelectorContacts?: Array>; @@ -94,8 +100,7 @@ export type ActivityFeedV2Props = { getAvatarUrl?: GetAvatarUrl; getMentionAsync?: (searchStr: string) => Promise>>; getTaskCollaborators?: (task: TaskNew) => Promise; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getViewer?: () => any; + getViewer?: () => ViewerHandle | null; hasTasks?: boolean; isDisabled?: boolean; isTimestampedCommentsEnabled?: boolean; From 928ca74259be99424854f0b6b3eea97dc41b4a4c Mon Sep 17 00:00:00 2001 From: Duncan Hsu Date: Thu, 25 Jun 2026 12:05:30 -0700 Subject: [PATCH 4/5] refactor(activity-feed-v2): revert some changes and change event names --- .../activity-feed-v2/ActivityFeedV2.tsx | 15 +++++---- .../__tests__/ActivityFeedV2.test.tsx | 32 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index a42b364c50..ef3ce23e2e 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -72,8 +72,6 @@ const ActivityFeedV2 = ({ }: ActivityFeedV2Props) => { const intl = useIntl(); const scrollHandle = useActivityFeedScroll(); - const scrollHandleRef = React.useRef(scrollHandle); - scrollHandleRef.current = scrollHandle; const currentUserId = currentUser?.id; const headerTitle = intl.formatMessage(commonMessages.sidebarActivityTitle); @@ -353,7 +351,7 @@ const ActivityFeedV2 = ({ if (!getViewer || !isVideo) return; const viewer = getViewer(); if (!viewer) return; - viewer.emit('commentmarkers', commentMarkers); + viewer.emit('comment_markers', commentMarkers); }, [commentMarkers, getViewer, isVideo]); React.useEffect(() => { @@ -363,13 +361,16 @@ const ActivityFeedV2 = ({ const handleMarkerSelect = ({ id }: { id: string }) => { requestAnimationFrame(() => { - scrollHandleRef.current?.scrollTo(id); + const el = document.querySelector(`[data-activity-id="${CSS.escape(id)}"]`); + if (el) { + el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } }); }; - viewer.addListener('commentmarkerselect', handleMarkerSelect); + viewer.addListener('comment_marker_select', handleMarkerSelect); return () => { - viewer.emit('commentmarkers', []); - viewer.removeListener('commentmarkerselect', handleMarkerSelect); + viewer.emit('comment_markers', []); + viewer.removeListener('comment_marker_select', handleMarkerSelect); }; }, [getViewer, isVideo]); diff --git a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx index 4b2df4bee1..7ad9e811a9 100644 --- a/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx @@ -1086,24 +1086,24 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { mockGetViewer.mockClear(); }); - test('should not emit commentmarkers when getViewer is not provided', () => { + test('should not emit comment_markers when getViewer is not provided', () => { renderComponentWithMarkers({ getViewer: undefined }); expect(mockViewer.emit).not.toHaveBeenCalled(); }); - test('should not emit commentmarkers when file is not a video', () => { + test('should not emit comment_markers when file is not a video', () => { renderComponentWithMarkers({ file: { extension: 'pdf', file_version: { id: '1' } } }); - expect(mockViewer.emit).not.toHaveBeenCalledWith('commentmarkers', expect.anything()); + expect(mockViewer.emit).not.toHaveBeenCalledWith('comment_markers', expect.anything()); }); - test('should not emit commentmarkers when getViewer returns null', () => { + test('should not emit comment_markers when getViewer returns null', () => { renderComponentWithMarkers({ getViewer: jest.fn(() => null) }); expect(mockViewer.emit).not.toHaveBeenCalled(); }); - test('should emit commentmarkers with timestamped comment data', () => { + test('should emit comment_markers with timestamped comment data', () => { renderComponentWithMarkers(); - expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', [ + expect(mockViewer.emit).toHaveBeenCalledWith('comment_markers', [ expect.objectContaining({ id: 'ts-comment-1', time: 5, @@ -1112,9 +1112,9 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { ]); }); - test('should emit commentmarkers with frame annotation data', () => { + test('should emit comment_markers with frame annotation data', () => { renderComponentWithMarkers({ feedItems: [frameAnnotation] as ActivityFeedV2Props['feedItems'] }); - expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', [ + expect(mockViewer.emit).toHaveBeenCalledWith('comment_markers', [ expect.objectContaining({ id: 'frame-ann-1', time: 10, @@ -1125,7 +1125,7 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { test('should include author avatar and initial in markers', () => { renderComponentWithMarkers(); - expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', [ + expect(mockViewer.emit).toHaveBeenCalledWith('comment_markers', [ expect.objectContaining({ initial: 'C', colorIndex: 2, @@ -1133,27 +1133,27 @@ describe('elements/content-sidebar/activity-feed-v2/ActivityFeedV2', () => { ]); }); - test('should register commentmarkerselect listener on viewer', () => { + test('should register comment_marker_select listener on viewer', () => { renderComponentWithMarkers(); - expect(mockViewer.addListener).toHaveBeenCalledWith('commentmarkerselect', expect.any(Function)); + expect(mockViewer.addListener).toHaveBeenCalledWith('comment_marker_select', expect.any(Function)); }); - test('should remove commentmarkerselect listener on unmount', () => { + test('should remove comment_marker_select listener on unmount', () => { const { unmount } = renderComponentWithMarkers(); mockViewer.emit.mockClear(); unmount(); - expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', []); - expect(mockViewer.removeListener).toHaveBeenCalledWith('commentmarkerselect', expect.any(Function)); + expect(mockViewer.emit).toHaveBeenCalledWith('comment_markers', []); + expect(mockViewer.removeListener).toHaveBeenCalledWith('comment_marker_select', expect.any(Function)); }); test('should not include non-timestamped comments in markers', () => { renderComponentWithMarkers({ feedItems: [mockComment] as ActivityFeedV2Props['feedItems'] }); - expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', []); + expect(mockViewer.emit).toHaveBeenCalledWith('comment_markers', []); }); test('should not include page-based annotations in markers', () => { renderComponentWithMarkers({ feedItems: [mockAnnotation] as ActivityFeedV2Props['feedItems'] }); - expect(mockViewer.emit).toHaveBeenCalledWith('commentmarkers', []); + expect(mockViewer.emit).toHaveBeenCalledWith('comment_markers', []); }); }); }); From 71af51c59485991fe69484a6c7f101881f47ce30 Mon Sep 17 00:00:00 2001 From: Duncan Hsu Date: Thu, 25 Jun 2026 13:33:12 -0700 Subject: [PATCH 5/5] refactor(activity-feed-v2): revert back to single useEffect --- .../activity-feed-v2/ActivityFeedV2.tsx | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx index ef3ce23e2e..9c0afc2ee1 100644 --- a/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx +++ b/src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx @@ -73,6 +73,8 @@ const ActivityFeedV2 = ({ const intl = useIntl(); const scrollHandle = useActivityFeedScroll(); const currentUserId = currentUser?.id; + const scrollHandleRef = React.useRef(null); + scrollHandleRef.current = scrollHandle; const headerTitle = intl.formatMessage(commonMessages.sidebarActivityTitle); const scrolledEntryIdRef = React.useRef(null); @@ -308,8 +310,11 @@ const ActivityFeedV2 = ({ ? { formattedTimestamp, isPressed: isTimestampPressed, onPressedChange } : undefined; - const commentMarkers = React.useMemo(() => { - if (!isVideo) return []; + React.useEffect(() => { + if (!getViewer || !isVideo) return undefined; + const viewer = getViewer(); + if (!viewer) return undefined; + const markers: Array<{ avatarUrl?: string; colorIndex?: number; @@ -344,35 +349,19 @@ const ActivityFeedV2 = ({ } } } - return markers; - }, [filteredItems, isVideo]); - - React.useEffect(() => { - if (!getViewer || !isVideo) return; - const viewer = getViewer(); - if (!viewer) return; - viewer.emit('comment_markers', commentMarkers); - }, [commentMarkers, getViewer, isVideo]); - - React.useEffect(() => { - if (!getViewer || !isVideo) return undefined; - const viewer = getViewer(); - if (!viewer) return undefined; + viewer.emit('comment_markers', markers); const handleMarkerSelect = ({ id }: { id: string }) => { requestAnimationFrame(() => { - const el = document.querySelector(`[data-activity-id="${CSS.escape(id)}"]`); - if (el) { - el.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } + scrollHandleRef.current?.scrollTo(id); }); }; viewer.addListener('comment_marker_select', handleMarkerSelect); return () => { - viewer.emit('comment_markers', []); viewer.removeListener('comment_marker_select', handleMarkerSelect); + viewer.emit('comment_markers', []); }; - }, [getViewer, isVideo]); + }, [filteredItems, getViewer, isVideo]); const handleCommentPost = React.useCallback( async (content: unknown) => {