Skip to content
26 changes: 21 additions & 5 deletions components/hearing/HearingDetails.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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)`
Expand Down Expand Up @@ -51,9 +55,11 @@ export const HearingDetails = ({
hearingData: HearingData
}) => {
const { t } = useTranslation(["common", "hearing"])
const [transcriptData, setTranscriptData] = useState<Paragraph[] | null>(null)
const router = useRouter()

const [transcriptData, setTranscriptData] = useState<Paragraph[] | null>(null)
const [videoLoaded, setVideoLoaded] = useState(false)

const handleVideoLoad = () => {
setVideoLoaded(true)
}
Expand All @@ -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
Expand Down Expand Up @@ -169,6 +184,7 @@ export const HearingDetails = ({

{transcriptData ? (
<Transcriptions
hearingId={hearingId}
transcriptData={transcriptData}
setCurTimeVideo={setCurTimeVideo}
videoLoaded={videoLoaded}
Expand Down
124 changes: 101 additions & 23 deletions components/hearing/Transcriptions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { faMagnifyingGlass, faTimes } from "@fortawesome/free-solid-svg-icons"
import {
faMagnifyingGlass,
faShareAlt,
faTimes
} from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useRouter } from "next/router"
import { useTranslation } from "next-i18next"
import React, { forwardRef, useEffect, useRef, useState } from "react"
import styled from "styled-components"
import { Col, Container, Row } from "../bootstrap"
import { Paragraph, formatMilliseconds } from "./hearing"
import {
Paragraph,
convertToString,
formatMilliseconds,
formatTotalSeconds
} from "./hearing"
import { CopyButton } from "components/buttons"

const ClearButton = styled(FontAwesomeIcon)`
position: absolute;
Expand Down Expand Up @@ -116,11 +127,13 @@ const TranscriptRow = styled(Row)`
`

export const Transcriptions = ({
hearingId,
transcriptData,
setCurTimeVideo,
videoLoaded,
videoRef
}: {
hearingId: string
transcriptData: Paragraph[]
setCurTimeVideo: any
videoLoaded: boolean
Expand All @@ -132,11 +145,40 @@ export const Transcriptions = ({
const transcriptRefs = useRef(new Map())
const [searchTerm, setSearchTerm] = useState("")
const [filteredData, setFilteredData] = useState<Paragraph[]>([])
const [initialScrollTarget, setInitialScrollTarget] = useState<number | null>(
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 =>
Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -217,6 +278,7 @@ export const Transcriptions = ({
<TranscriptItem
key={index}
element={element}
hearingId={hearingId}
highlightedId={highlightedId}
index={index}
ref={elem => {
Expand Down Expand Up @@ -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
Expand All @@ -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, "\\$&")
Expand Down Expand Up @@ -316,6 +381,19 @@ const TranscriptItem = forwardRef(function TranscriptItem(
</Row>
</TimestampCol>
<Col className={`pt-1`}>{highlightText(element.text, searchTerm)}</Col>
<Col xs="1">
<CopyButton
key="copy"
variant="outline-secondary"
text={`http://localhost:3000/hearing/${hearingId}?t=${formatTotalSeconds(
element.start
)}`}
className={`copy my-1 px-1 py-0`}
format="text/plain"
>
<FontAwesomeIcon icon={faShareAlt} />
</CopyButton>
</Col>
</TranscriptRow>
)
})
16 changes: 16 additions & 0 deletions components/hearing/hearing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HearingData | null> {
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion pages/hearing/[hearingId].tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down