Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions components/buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,48 @@ export const CopyButton = ({
)
}

export const ShareLinkButton = ({
text,
tooltipDurationMs = 1000,
children,
format = "text/html",
...props
}: ButtonProps & {
text: string
tooltipDurationMs?: number
format?: string
}) => {
const { t } = useTranslation("common")
const [show, setShow] = useState(false)
const target = useRef(null)
const closeTimeout = useRef<any>()
return (
<>
<CopyToClipboard
text={text}
options={{ format: format }}
onCopy={(_, success) => {
if (success) {
clearTimeout(closeTimeout.current)
setShow(true)
closeTimeout.current = setTimeout(
() => setShow(false),
tooltipDurationMs
)
}
}}
>
<Button ref={target} style={{ color: "#737373" }} variant="" {...props}>
{children}
</Button>
</CopyToClipboard>
<Overlay target={target} show={show} placement="top">
{props => <Tooltip {...props}>{t("copiedToClipboard")}</Tooltip>}
</Overlay>
</>
)
}

export const GearIcon = (
<div className={`py-0`}>
<svg
Expand Down
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
129 changes: 107 additions & 22 deletions components/hearing/Transcriptions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { faMagnifyingGlass, faTimes } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import ShareIcon from "@mui/icons-material/Share"
import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined"
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 { ShareLinkButton } from "components/buttons"

const ClearButton = styled(FontAwesomeIcon)`
position: absolute;
Expand Down Expand Up @@ -116,11 +125,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 +143,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 +185,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 +276,7 @@ export const Transcriptions = ({
<TranscriptItem
key={index}
element={element}
hearingId={hearingId}
highlightedId={highlightedId}
index={index}
ref={elem => {
Expand Down Expand Up @@ -249,12 +309,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 +337,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 All @@ -290,6 +353,8 @@ const TranscriptItem = forwardRef(function TranscriptItem(
)
}

const [isHovered, setIsHovered] = useState(false)

return (
<TranscriptRow
className={
Expand All @@ -316,6 +381,26 @@ const TranscriptItem = forwardRef(function TranscriptItem(
</Row>
</TimestampCol>
<Col className={`pt-1`}>{highlightText(element.text, searchTerm)}</Col>
<Col xs="1" className={`my-1 px-0`}>
{isHighlighted(index) ? (
<>
<ShareLinkButton
key="copy"
text={`http://localhost:3000/hearing/${hearingId}?t=${formatTotalSeconds(
element.start
)}`}
className={`copy my-1 px-1 py-0`}
format="text/plain"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{isHovered ? <ShareIcon /> : <ShareOutlinedIcon />}
</ShareLinkButton>
</>
) : (
<></>
)}
</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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,14 @@
]
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@emotion/weak-memoize": "^0.3.1",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7",
"@popperjs/core": "^2.11.8",
"@react-aria/ssr": "^3.2.0",
"@react-aria/utils": "^3.13.1",
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
Loading