Add timestamp field to video report dialog (#11339)
This commit is contained in:
@@ -2,8 +2,7 @@ import {useImperativeHandle, useRef, useState} from 'react'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@bsky.app/video'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
@@ -16,6 +15,7 @@ import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {KeepAwake} from '#/components/KeepAwake'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {GifPresentationControls} from '../GifPresentationControls'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
@@ -39,11 +39,13 @@ export function VideoEmbedInnerNative({
|
||||
*/
|
||||
onError?: (error: string) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const videoRef = useRef<BlueskyVideoView>(null)
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
const isWithinMessage = useIsWithinMessage()
|
||||
const [muted, setMuted] = useVideoMuteState()
|
||||
const reportDialogMetadata = useReportDialogMetadataContext()
|
||||
const maxTimeRemainingSeconds = useRef(0)
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [timeRemaining, setTimeRemaining] = useState(0)
|
||||
@@ -84,16 +86,30 @@ export function VideoEmbedInnerNative({
|
||||
setIsPlaying(e.nativeEvent.status === 'playing')
|
||||
}}
|
||||
onTimeRemainingChange={e => {
|
||||
setTimeRemaining(e.nativeEvent.timeRemaining)
|
||||
const {timeRemaining} = e.nativeEvent
|
||||
setTimeRemaining(timeRemaining)
|
||||
if (
|
||||
!isGif &&
|
||||
reportDialogMetadata &&
|
||||
Number.isFinite(timeRemaining) &&
|
||||
timeRemaining >= 0
|
||||
) {
|
||||
maxTimeRemainingSeconds.current = Math.max(
|
||||
maxTimeRemainingSeconds.current,
|
||||
timeRemaining,
|
||||
)
|
||||
reportDialogMetadata.current.videoTimestampSeconds = Math.max(
|
||||
0,
|
||||
maxTimeRemainingSeconds.current - timeRemaining,
|
||||
)
|
||||
}
|
||||
}}
|
||||
onError={e => {
|
||||
onError?.(e.nativeEvent.error)
|
||||
setError(e.nativeEvent.error)
|
||||
}}
|
||||
ref={videoRef}
|
||||
accessibilityLabel={
|
||||
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
|
||||
}
|
||||
accessibilityLabel={embed.alt ? l`Video: ${embed.alt}` : l`Video`}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
{isGif ? (
|
||||
@@ -144,7 +160,7 @@ function VideoPresentationControls({
|
||||
timeRemaining: number
|
||||
isPlaying: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const t = useTheme()
|
||||
const [muted] = useVideoMuteState()
|
||||
|
||||
@@ -159,14 +175,14 @@ function VideoPresentationControls({
|
||||
<Pressable
|
||||
onPress={enterFullscreen}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel={_(msg`Video`)}
|
||||
accessibilityHint={_(msg`Enters full screen`)}
|
||||
accessibilityLabel={l`Video`}
|
||||
accessibilityHint={l`Enters full screen`}
|
||||
accessibilityRole="button"
|
||||
/>
|
||||
<ControlButton
|
||||
onPress={togglePlayback}
|
||||
label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
|
||||
accessibilityHint={_(msg`Plays or pauses the video`)}
|
||||
label={isPlaying ? l`Pause` : l`Play`}
|
||||
accessibilityHint={l`Plays or pauses the video`}
|
||||
style={{left: 6}}>
|
||||
{isPlaying ? (
|
||||
<PauseIcon width={13} fill={t.palette.white} />
|
||||
@@ -175,15 +191,14 @@ function VideoPresentationControls({
|
||||
)}
|
||||
</ControlButton>
|
||||
{showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
|
||||
|
||||
<ControlButton
|
||||
onPress={toggleMuted}
|
||||
label={
|
||||
muted
|
||||
? _(msg({message: `Unmute`, context: 'video'}))
|
||||
: _(msg({message: `Mute`, context: 'video'}))
|
||||
? l({message: `Unmute`, context: 'video'})
|
||||
: l({message: `Mute`, context: 'video'})
|
||||
}
|
||||
accessibilityHint={_(msg`Toggles the sound`)}
|
||||
accessibilityHint={l`Toggles the sound`}
|
||||
style={{right: 6}}>
|
||||
{muted ? (
|
||||
<MuteIcon width={13} fill={t.palette.white} />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {useCallback, useEffect, useId, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
import type * as HlsTypes from 'hls.js'
|
||||
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {useReportDialogMetadataContext} from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
|
||||
import * as BandwidthEstimate from './bandwidth-estimate'
|
||||
import {
|
||||
HLSFatalError,
|
||||
@@ -36,9 +36,10 @@ export function VideoEmbedInnerWeb({
|
||||
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
|
||||
const [hlsLoading, setHlsLoading] = useState(false)
|
||||
const figId = useId()
|
||||
const {_} = useLingui()
|
||||
const {t: l} = useLingui()
|
||||
const [isFullscreen] = useFullscreen(containerRef)
|
||||
const isGif = embed.presentation === 'gif'
|
||||
const reportDialogMetadata = useReportDialogMetadataContext()
|
||||
|
||||
// send error up to error boundary
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
@@ -63,7 +64,7 @@ export function VideoEmbedInnerWeb({
|
||||
return (
|
||||
<View
|
||||
style={[a.flex_1, a.rounded_md, a.overflow_hidden]}
|
||||
accessibilityLabel={_(msg`Embedded video player`)}
|
||||
accessibilityLabel={l`Embedded video player`}
|
||||
accessibilityHint="">
|
||||
<div ref={containerRef} style={{height: '100%', width: '100%'}}>
|
||||
<figure style={{margin: 0, position: 'absolute', inset: 0}}>
|
||||
@@ -76,7 +77,16 @@ export function VideoEmbedInnerWeb({
|
||||
muted={embed.presentation === 'gif' || !focused}
|
||||
aria-labelledby={embed.alt ? figId : undefined}
|
||||
onTimeUpdate={e => {
|
||||
lastKnownTime.current = e.currentTarget.currentTime
|
||||
const currentTime = e.currentTarget.currentTime
|
||||
lastKnownTime.current = currentTime
|
||||
if (
|
||||
!isGif &&
|
||||
reportDialogMetadata &&
|
||||
Number.isFinite(currentTime) &&
|
||||
currentTime >= 0
|
||||
) {
|
||||
reportDialogMetadata.current.videoTimestampSeconds = currentTime
|
||||
}
|
||||
}}
|
||||
loop={loop}
|
||||
/>
|
||||
@@ -141,9 +151,10 @@ type CachedPromise<T> = Promise<T> & {value: undefined | T}
|
||||
const promiseForHls = import(
|
||||
// @ts-ignore
|
||||
'hls.js/dist/hls.min'
|
||||
// oxlint-disable-next-line typescript/no-unsafe-member-access
|
||||
).then(mod => mod.default) as CachedPromise<typeof HlsTypes.default>
|
||||
promiseForHls.value = undefined
|
||||
promiseForHls.then(Hls => {
|
||||
void promiseForHls.then(Hls => {
|
||||
promiseForHls.value = Hls
|
||||
})
|
||||
|
||||
@@ -166,7 +177,7 @@ function useHLS({
|
||||
useEffect(() => {
|
||||
if (!Hls) {
|
||||
setHlsLoading(true)
|
||||
promiseForHls.then(loadedHls => {
|
||||
void promiseForHls.then(loadedHls => {
|
||||
setHls(() => loadedHls)
|
||||
setHlsLoading(false)
|
||||
})
|
||||
@@ -303,7 +314,7 @@ function useHLS({
|
||||
hls.on(Hls.Events.ERROR, (_event, data) => {
|
||||
if (data.fatal) {
|
||||
if (
|
||||
data.details === 'manifestLoadError' &&
|
||||
(data.details as string) === 'manifestLoadError' &&
|
||||
data.response?.code === 404
|
||||
) {
|
||||
setError(new VideoNotFoundError())
|
||||
|
||||
@@ -3,11 +3,11 @@ import {View} from 'react-native'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {formatTime} from '#/lib/media/video/formatTime'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {IS_WEB_FIREFOX, IS_WEB_TOUCH_DEVICE} from '#/env'
|
||||
import {formatTime} from './utils'
|
||||
|
||||
export function Scrubber({
|
||||
duration,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {formatTime} from '#/lib/media/video/formatTime'
|
||||
import {clamp} from '#/lib/numbers'
|
||||
import {
|
||||
useAutoplayDisabled,
|
||||
@@ -31,7 +32,7 @@ import {GifPresentationControls} from '../../GifPresentationControls'
|
||||
import {TimeIndicator} from '../TimeIndicator'
|
||||
import {ControlButton} from './ControlButton'
|
||||
import {Scrubber} from './Scrubber'
|
||||
import {formatTime, useVideoElement} from './utils'
|
||||
import {useVideoElement} from './utils'
|
||||
import {type ControlsProps} from './VideoControls.shared'
|
||||
import {VolumeControl} from './VolumeControl'
|
||||
|
||||
|
||||
@@ -238,16 +238,3 @@ export function useVideoElement(ref: RefObject<HTMLVideoElement | null>) {
|
||||
canPlay,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTime(time: number) {
|
||||
if (isNaN(time)) {
|
||||
return '--'
|
||||
}
|
||||
|
||||
time = Math.round(time)
|
||||
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = String(time % 60).padStart(2, '0')
|
||||
|
||||
return `${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {GalleryBleed} from '#/components/images/Gallery'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext'
|
||||
import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed'
|
||||
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
||||
import {RichText} from '#/components/RichText'
|
||||
@@ -311,7 +312,7 @@ export function QuoteEmbed({
|
||||
} = useInteractionState()
|
||||
|
||||
const contents = (
|
||||
<>
|
||||
<ReportDialogMetadataContext.Provider key={quote.uri}>
|
||||
<PostMeta
|
||||
author={quote.author}
|
||||
moderation={moderation}
|
||||
@@ -350,7 +351,7 @@ export function QuoteEmbed({
|
||||
post={quote}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ReportDialogMetadataContext.Provider>
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user