diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index f79bdde5c..e6e482d34 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -1,9 +1,8 @@ -import { doc, getDoc } from "firebase/firestore" +import { useRouter } from "next/router" import { Trans, useTranslation } from "next-i18next" -import { useCallback, useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import styled from "styled-components" import { Col, Container, Image, Row } from "../bootstrap" -import { firestore } from "../firebase" import * as links from "../links" import { committeeURL, External } from "../links" import { @@ -12,7 +11,12 @@ import { FeatureCalloutButton } from "../shared/CommonComponents" import { HearingSidebar } from "./HearingSidebar" -import { HearingData, Paragraph, fetchTranscriptionData } from "./hearing" +import { + HearingData, + Paragraph, + convertToString, + fetchTranscriptionData +} from "./hearing" import { Transcriptions } from "./Transcriptions" const LegalContainer = styled(Container)` @@ -51,9 +55,11 @@ export const HearingDetails = ({ hearingData: HearingData }) => { const { t } = useTranslation(["common", "hearing"]) - const [transcriptData, setTranscriptData] = useState(null) + const router = useRouter() + const [transcriptData, setTranscriptData] = useState(null) const [videoLoaded, setVideoLoaded] = useState(false) + const handleVideoLoad = () => { setVideoLoaded(true) } @@ -63,6 +69,15 @@ export const HearingDetails = ({ videoRef.current ? (videoRef.current.currentTime = value) : null } + useEffect(() => { + const startTime = router.query.t + const resultString: string = convertToString(startTime) + + if (startTime && videoRef.current) { + setCurTimeVideo(parseInt(resultString, 10)) + } + }, [router.query.t, videoRef.current]) + useEffect(() => { ;(async function () { if (!videoTranscriptionId || transcriptData !== null) return @@ -169,6 +184,7 @@ export const HearingDetails = ({ {transcriptData ? ( ([]) + const [initialScrollTarget, setInitialScrollTarget] = useState( + null + ) + const hasScrolledToInitial = useRef(false) const handleClearInput = () => { setSearchTerm("") } + // Shared function to scroll to a transcript index + const scrollToTranscript = (index: number) => { + const container = containerRef.current + const elem = transcriptRefs.current.get(index) + + if (elem && container) { + const elemTop = elem.offsetTop - container.offsetTop + const elemBottom = elemTop + elem.offsetHeight + const viewTop = container.scrollTop + const viewBottom = viewTop + container.clientHeight + + if (elemTop < viewTop) { + container.scrollTo({ + top: elemTop, + behavior: "smooth" + }) + } else if (elemBottom > viewBottom) { + container.scrollTo({ + top: elemBottom - container.clientHeight, + behavior: "smooth" + }) + } + } + } + useEffect(() => { setFilteredData( transcriptData.filter(el => @@ -145,32 +187,51 @@ export const Transcriptions = ({ ) }, [transcriptData, searchTerm]) + const router = useRouter() + const startTime = router.query.t + const resultString: string = convertToString(startTime) + + let currentIndex = transcriptData.findIndex( + element => parseInt(resultString, 10) <= element.end / 1000 + ) + + // Set the initial scroll target when we have a startTime and transcripts + useEffect(() => { + if ( + startTime && + transcriptData.length > 0 && + currentIndex !== -1 && + !hasScrolledToInitial.current + ) { + setInitialScrollTarget(currentIndex) + } + }, [startTime, transcriptData, currentIndex]) + + // Scroll to the initial target when the ref becomes available + useEffect(() => { + if (initialScrollTarget !== null && !searchTerm) { + const elem = transcriptRefs.current.get(initialScrollTarget) + + if (elem) { + setHighlightedId(initialScrollTarget) + scrollToTranscript(initialScrollTarget) + hasScrolledToInitial.current = true + setInitialScrollTarget(null) + } + } + }, [initialScrollTarget, transcriptRefs.current.size, searchTerm]) + useEffect(() => { const handleTimeUpdate = () => { - const currentIndex = transcriptData.findIndex( - element => videoRef.current.currentTime <= element.end / 1000 - ) + videoLoaded + ? (currentIndex = transcriptData.findIndex( + element => videoRef.current.currentTime <= element.end / 1000 + )) + : null if (containerRef.current && currentIndex !== highlightedId) { setHighlightedId(currentIndex) if (currentIndex !== -1 && !searchTerm) { - const container = containerRef.current - const elem = transcriptRefs.current.get(currentIndex) - const elemTop = elem.offsetTop - container.offsetTop - const elemBottom = elemTop + elem.offsetHeight - const viewTop = container.scrollTop - const viewBottom = viewTop + container.clientHeight - - if (elemTop < viewTop) { - container.scrollTo({ - top: elemTop, - behavior: "smooth" - }) - } else if (elemBottom > viewBottom) { - container.scrollTo({ - top: elemBottom - container.clientHeight, - behavior: "smooth" - }) - } + scrollToTranscript(currentIndex) } } } @@ -217,6 +278,7 @@ export const Transcriptions = ({ { @@ -249,12 +311,14 @@ export const Transcriptions = ({ const TranscriptItem = forwardRef(function TranscriptItem( { element, + hearingId, highlightedId, index, setCurTimeVideo, searchTerm }: { element: Paragraph + hearingId: string highlightedId: number index: number setCurTimeVideo: any @@ -275,6 +339,7 @@ const TranscriptItem = forwardRef(function TranscriptItem( const isHighlighted = (index: number): boolean => { return index === highlightedId } + const highlightText = (text: string, term: string) => { if (!term) return text const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") @@ -316,6 +381,19 @@ const TranscriptItem = forwardRef(function TranscriptItem( {highlightText(element.text, searchTerm)} + + + + + ) }) diff --git a/components/hearing/hearing.ts b/components/hearing/hearing.ts index 4ba2b3439..c87bba25c 100644 --- a/components/hearing/hearing.ts +++ b/components/hearing/hearing.ts @@ -29,6 +29,15 @@ export type Paragraph = { text: string } +export const convertToString = ( + value: string | string[] | undefined +): string => { + if (Array.isArray(value)) { + return value.join(", ") + } + return value ?? "" +} + export async function fetchHearingData( hearingId: string ): Promise { @@ -98,6 +107,13 @@ export function formatMilliseconds(ms: number): string { } } +export function formatTotalSeconds(ms: number): string { + const totalSeconds = Math.floor(ms / 1000) + const formattedSeconds = String(totalSeconds) + + return `${formattedSeconds}` +} + export function formatVTTTimestamp(ms: number): string { const totalSeconds = Math.floor(ms / 1000) const milliseconds = ms % 1000 diff --git a/pages/hearing/[hearingId].tsx b/pages/hearing/[hearingId].tsx index 73a0ad6b7..84263d4e1 100644 --- a/pages/hearing/[hearingId].tsx +++ b/pages/hearing/[hearingId].tsx @@ -1,5 +1,4 @@ import { GetServerSideProps } from "next" -import { useRouter } from "next/router" import { serverSideTranslations } from "next-i18next/serverSideTranslations" import { z } from "zod" import { flags } from "components/featureFlags"