diff --git a/static/app-strings.json b/static/app-strings.json index fa3fb23ff..5f1059235 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -2054,5 +2054,7 @@ "Item removed from Watch Later": "Item removed from Watch Later", "Skip Navigation": "Skip Navigation", "In Favorites": "In Favorites", + "%title% by %channelTitle%, %mediaDuration%": "%title% by %channelTitle%, %mediaDuration%", + "%title% by %channelTitle%": "%title% by %channelTitle%", "--end--": "--end--" } diff --git a/ui/component/claimPreviewTile/index.js b/ui/component/claimPreviewTile/index.js index 256ca5f45..878be3e22 100644 --- a/ui/component/claimPreviewTile/index.js +++ b/ui/component/claimPreviewTile/index.js @@ -14,20 +14,28 @@ import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectShowMatureContent } from 'redux/selectors/settings'; import ClaimPreviewTile from './view'; +import formatMediaDuration from 'util/formatMediaDuration'; -const select = (state, props) => ({ - claim: props.uri && makeSelectClaimForUri(props.uri)(state), - channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state), - isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), - thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state), - title: props.uri && makeSelectTitleForUri(props.uri)(state), - blackListedOutpoints: selectBlackListedOutpoints(state), - filteredOutpoints: selectFilteredOutpoints(state), - blockedChannelUris: selectMutedChannels(state), - showMature: selectShowMatureContent(state), - isMature: makeSelectClaimIsNsfw(props.uri)(state), - isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), -}); +const select = (state, props) => { + const claim = props.uri && makeSelectClaimForUri(props.uri)(state); + const media = claim && claim.value && (claim.value.video || claim.value.audio); + const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true }); + + return { + claim, + mediaDuration, + channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state), + isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), + thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state), + title: props.uri && makeSelectTitleForUri(props.uri)(state), + blackListedOutpoints: selectBlackListedOutpoints(state), + filteredOutpoints: selectFilteredOutpoints(state), + blockedChannelUris: selectMutedChannels(state), + showMature: selectShowMatureContent(state), + isMature: makeSelectClaimIsNsfw(props.uri)(state), + isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), + }; +}; const perform = (dispatch) => ({ resolveUri: (uri) => dispatch(doResolveUri(uri)), diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx index 6a1e3b4ae..c238b44fb 100644 --- a/ui/component/claimPreviewTile/view.jsx +++ b/ui/component/claimPreviewTile/view.jsx @@ -21,6 +21,7 @@ import CollectionPreviewOverlay from 'component/collectionPreviewOverlay'; type Props = { uri: string, claim: ?Claim, + mediaDuration?: string, resolveUri: (string) => void, isResolvingUri: boolean, history: { push: (string) => void }, @@ -72,6 +73,7 @@ function ClaimPreviewTile(props: Props) { showNoSourceClaims, isLivestream, collectionId, + mediaDuration, } = props; const isRepost = claim && claim.repost_channel_url; const isCollection = claim && claim.value_type === 'collection'; @@ -114,6 +116,18 @@ function ClaimPreviewTile(props: Props) { const signingChannel = claim && claim.signing_channel; const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url; + const channelTitle = signingChannel && (signingChannel.value.title || signingChannel.name); + + // Aria-label value for claim preview + let ariaLabelData = title; + + if (!isChannel && channelTitle) { + if (mediaDuration) { + ariaLabelData = __('%title% by %channelTitle%, %mediaDuration%', { title, channelTitle, mediaDuration }); + } else { + ariaLabelData = __('%title% by %channelTitle%', { title, channelTitle }); + } + } function handleClick(e) { if (navigateUrl) { @@ -226,7 +240,7 @@ function ClaimPreviewTile(props: Props) {
- +

{isChannel && ( diff --git a/ui/component/videoDuration/view.jsx b/ui/component/videoDuration/view.jsx index 625924cac..086cd4352 100644 --- a/ui/component/videoDuration/view.jsx +++ b/ui/component/videoDuration/view.jsx @@ -1,6 +1,6 @@ // @flow import React from 'react'; - +import formatMediaDuration from 'util/formatMediaDuration'; type Props = { claim: ?StreamClaim, className?: string, @@ -9,19 +9,11 @@ type Props = { function VideoDuration(props: Props) { const { claim, className } = props; - const video = claim && claim.value && (claim.value.video || claim.value.audio); + const media = claim && claim.value && (claim.value.video || claim.value.audio); let duration; - if (video && video.duration) { + if (media && media.duration) { // $FlowFixMe - let date = new Date(null); - date.setSeconds(video.duration); - let timeString = date.toISOString().substr(11, 8); - - if (timeString.startsWith('00:')) { - timeString = timeString.substr(3); - } - - duration = timeString; + duration = formatMediaDuration(media.duration); } return duration ? {duration} : null; diff --git a/ui/util/formatMediaDuration.js b/ui/util/formatMediaDuration.js new file mode 100644 index 000000000..dc3f1f24b --- /dev/null +++ b/ui/util/formatMediaDuration.js @@ -0,0 +1,24 @@ +import moment from 'moment'; + +export default function formatMediaDuration(duration = 0, config) { + const options = { + screenReader: false, + ...config, + }; + + // Optimize for screen readers + if (options.screenReader) { + return moment.utc(moment.duration(duration, 'seconds').asMilliseconds()).format('HH:mm:ss'); + } + + // Normal format + let date = new Date(null); + date.setSeconds(duration); + + let timeString = date.toISOString().substr(11, 8); + if (timeString.startsWith('00:')) { + timeString = timeString.substr(3); + } + + return timeString; +}