From 8fe9cfafbc12fcef127877347e8ec8a4e1491694 Mon Sep 17 00:00:00 2001 From: Jeffrey Fisher Date: Tue, 5 May 2020 20:05:59 -0700 Subject: [PATCH] Allow video sharing with start timestamp Closes #3122 --- static/app-strings.json | 2 +- ui/component/embedTextArea/view.jsx | 7 +- ui/component/socialShare/index.js | 2 + ui/component/socialShare/view.jsx | 108 ++++++++++++++++++++++++---- ui/scss/component/section.scss | 12 ++++ ui/util/lbrytv.js | 5 +- ui/util/time.js | 34 +++++++++ ui/util/url.js | 38 ++++++++++ 8 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 ui/util/time.js diff --git a/static/app-strings.json b/static/app-strings.json index 10caef396..8332d40d9 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1201,4 +1201,4 @@ "Share this channel": "Share this channel", "File preview": "File preview", "Go Home": "Go Home" -} \ No newline at end of file +} diff --git a/ui/component/embedTextArea/view.jsx b/ui/component/embedTextArea/view.jsx index 8d2daecd9..224754db9 100644 --- a/ui/component/embedTextArea/view.jsx +++ b/ui/component/embedTextArea/view.jsx @@ -11,14 +11,16 @@ type Props = { doToast: ({ message: string }) => void, label?: string, claim: Claim, + includeStartTime: boolean, + startTime: number, }; export default function EmbedTextArea(props: Props) { - const { doToast, snackMessage, label, claim } = props; + const { doToast, snackMessage, label, claim, includeStartTime, startTime } = props; const { claim_id: claimId, name } = claim; const input = useRef(); - const streamUrl = generateEmbedUrl(name, claimId); + const streamUrl = generateEmbedUrl(name, claimId, includeStartTime, startTime); let embedText = ``; function copyToClipboard() { @@ -47,6 +49,7 @@ export default function EmbedTextArea(props: Props) { value={embedText || ''} ref={input} onFocus={onFocus} + readOnly />
diff --git a/ui/component/socialShare/index.js b/ui/component/socialShare/index.js index b91dcdd6c..839fb5f42 100644 --- a/ui/component/socialShare/index.js +++ b/ui/component/socialShare/index.js @@ -2,12 +2,14 @@ import { connect } from 'react-redux'; import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; import SocialShare from './view'; import { selectUserInviteReferralCode, selectUser } from 'lbryinc'; +import { makeSelectContentPositionForUri } from 'redux/selectors/content'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), referralCode: selectUserInviteReferralCode(state), user: selectUser(state), title: makeSelectTitleForUri(props.uri)(state), + position: makeSelectContentPositionForUri(props.uri)(state), }); export default connect(select)(SocialShare); diff --git a/ui/component/socialShare/view.jsx b/ui/component/socialShare/view.jsx index c403abb5b..068befc76 100644 --- a/ui/component/socialShare/view.jsx +++ b/ui/component/socialShare/view.jsx @@ -6,6 +6,9 @@ import CopyableText from 'component/copyableText'; import EmbedTextArea from 'component/embedTextArea'; import { generateDownloadUrl } from 'util/lbrytv'; import useIsMobile from 'effects/use-is-mobile'; +import { FormField } from 'component/common/form'; +import { hmsToSeconds, secondsToHms } from 'util/time'; +import { generateLbryUrl, generateLbryWebUrl, generateEncodedLbryURL, generateOpenDotLbryDotComUrl } from 'util/url'; const IOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); const SUPPORTS_SHARE_API = typeof navigator.share !== 'undefined'; @@ -16,29 +19,56 @@ type Props = { webShareable: boolean, referralCode: string, user: any, + position: number, }; function SocialShare(props: Props) { - const { claim, title, referralCode, user, webShareable } = props; + const { claim, title, referralCode, user, webShareable, position } = props; const [showEmbed, setShowEmbed] = React.useState(false); const [showExtra, setShowExtra] = React.useState(false); + const [includeStartTime, setincludeStartTime]: [boolean, any] = React.useState(false); + const [startTime, setStartTime]: [string, any] = React.useState(secondsToHms(position)); + const [startTimeSeconds, setStartTimeSeconds]: [number, any] = React.useState(Math.floor(position)); const isMobile = useIsMobile(); + let canonicalUrl = 'lbry://'; + let permanentUrl = 'lbry://'; + let name = ''; + let claimId = ''; + + if (claim) { + canonicalUrl = claim.canonical_url; + permanentUrl = claim.permanent_url; + name = claim.name; + claimId = claim.claim_id; + } + + const isChannel = claim.value_type === 'channel'; + const rewardsApproved = user && user.is_reward_approved; + const OPEN_URL = 'https://open.lbry.com/'; + const lbryUrl: string = generateLbryUrl(canonicalUrl, permanentUrl); + const lbryWebUrl: string = generateLbryWebUrl(lbryUrl); + const [encodedLbryURL, setEncodedLbryURL]: [string, any] = React.useState( + generateEncodedLbryURL(OPEN_URL, lbryWebUrl, includeStartTime, startTime) + ); + const [openDotLbryDotComUrl, setOpenDotLbryDotComUrl]: [string, any] = React.useState( + generateOpenDotLbryDotComUrl( + OPEN_URL, + lbryWebUrl, + canonicalUrl, + permanentUrl, + referralCode, + rewardsApproved, + includeStartTime, + startTime + ) + ); + const downloadUrl = `${generateDownloadUrl(name, claimId)}`; + if (!claim) { return null; } - const { canonical_url: canonicalUrl, permanent_url: permanentUrl, name, claim_id: claimId } = claim; - const isChannel = claim.value_type === 'channel'; - const rewardsApproved = user && user.is_reward_approved; - const OPEN_URL = 'https://open.lbry.com/'; - const lbryUrl = canonicalUrl ? canonicalUrl.split('lbry://')[1] : permanentUrl.split('lbry://')[1]; - const lbryWebUrl = lbryUrl.replace(/#/g, ':'); - const encodedLbryURL: string = `${OPEN_URL}${encodeURIComponent(lbryWebUrl)}`; - const referralParam: string = referralCode && rewardsApproved ? `?r=${referralCode}` : ''; - const openDotLbryDotComUrl: string = `${OPEN_URL}${lbryWebUrl}${referralParam}`; - const downloadUrl = `${generateDownloadUrl(name, claimId)}`; - function handleWebShareClick() { if (navigator.share) { navigator.share({ @@ -48,9 +78,54 @@ function SocialShare(props: Props) { } } + function handleTimeCheckboxChange(checked) { + setincludeStartTime(checked); + updateUrls(checked, startTimeSeconds); + } + + function handleTimeChange(value) { + setStartTime(value); + const startSeconds = hmsToSeconds(value); + setStartTimeSeconds(startSeconds); + updateUrls(true, startSeconds); + } + + function updateUrls(includeStartTime, startTime) { + setOpenDotLbryDotComUrl( + generateOpenDotLbryDotComUrl( + OPEN_URL, + lbryWebUrl, + canonicalUrl, + permanentUrl, + referralCode, + rewardsApproved, + includeStartTime, + startTime + ) + ); + + setEncodedLbryURL(generateEncodedLbryURL(OPEN_URL, lbryWebUrl, includeStartTime, startTime)); + } + return ( +
+ handleTimeCheckboxChange(!includeStartTime)} + checked={includeStartTime} + label={__('Start at')} + /> + handleTimeChange(event.target.value)} + /> +
)} - {showEmbed && } + {showEmbed && ( + + )} {showExtra && (
diff --git a/ui/scss/component/section.scss b/ui/scss/component/section.scss index 649819c74..959f8b8cf 100644 --- a/ui/scss/component/section.scss +++ b/ui/scss/component/section.scss @@ -138,3 +138,15 @@ .section__actions--no-margin { margin-top: 0; } + +.section__start-at { + display: flex; + margin-top: var(--spacing-large); + fieldset-section { + width: 6em; + margin-top: 0; + } + .checkbox { + margin-right: 10px; + } +} diff --git a/ui/util/lbrytv.js b/ui/util/lbrytv.js index 9ff34c085..ec639cf2c 100644 --- a/ui/util/lbrytv.js +++ b/ui/util/lbrytv.js @@ -6,8 +6,9 @@ function generateStreamUrl(claimName, claimId) { return `${LBRY_TV_STREAMING_API}/content/claims/${claimName}/${claimId}/stream`; } -function generateEmbedUrl(claimName, claimId) { - return `${URL}/$/embed/${claimName}/${claimId}`; +function generateEmbedUrl(claimName, claimId, includeStartTime, startTime) { + const queryParam = includeStartTime ? `?t=${startTime}` : ''; + return `${URL}/$/embed/${claimName}/${claimId}${queryParam}`; } function generateDownloadUrl(claimName, claimId) { diff --git a/ui/util/time.js b/ui/util/time.js new file mode 100644 index 000000000..ca7127d1a --- /dev/null +++ b/ui/util/time.js @@ -0,0 +1,34 @@ +// @flow + +export function secondsToHms(seconds: number) { + seconds = Math.floor(seconds); + var hours = Math.floor(seconds / 3600); + var minutes = Math.floor(seconds / 60) % 60; + var seconds = seconds % 60; + + return [hours, minutes, seconds] + .map(v => (v < 10 ? '0' + v : v)) + .filter((v, i) => v !== '00' || i > 0) + .join(':'); +} + +export function hmsToSeconds(str: string) { + let timeParts = str.split(':'), + seconds = 0, + multiplier = 1; + + if (timeParts.length > 0) { + while (timeParts.length > 0) { + let nextPart = parseInt(timeParts.pop(), 10); + if (!Number.isInteger(nextPart)) { + nextPart = 0; + } + seconds += multiplier * nextPart; + multiplier *= 60; + } + } else { + seconds = 0; + } + + return seconds; +} diff --git a/ui/util/url.js b/ui/util/url.js index 427c7e8af..b325e47ea 100644 --- a/ui/util/url.js +++ b/ui/util/url.js @@ -70,3 +70,41 @@ exports.generateInitialUrl = hash => { } return url; }; + +exports.generateLbryUrl = (canonicalUrl, permanentUrl) => { + return canonicalUrl ? canonicalUrl.split('lbry://')[1] : permanentUrl.split('lbry://')[1]; +}; + +exports.generateLbryWebUrl = lbryUrl => { + return lbryUrl.replace(/#/g, ':'); +}; + +exports.generateEncodedLbryURL = (openUrl, lbryWebUrl, includeStartTime, startTime) => { + const queryParam = includeStartTime ? `?t=${startTime}` : ''; + const encodedPart = encodeURIComponent(`${lbryWebUrl}${queryParam}`); + return `${openUrl}${encodedPart}`; +}; + +exports.generateOpenDotLbryDotComUrl = ( + openUrl, + lbryWebUrl, + canonicalUrl, + permanentUrl, + referralCode, + rewardsApproved, + includeStartTime, + startTime +) => { + let urlParams = new URLSearchParams(); + if (referralCode && rewardsApproved) { + urlParams.append('r', referralCode); + } + + if (includeStartTime) { + urlParams.append('t', startTime.toString()); + } + + const urlParamsString = urlParams.toString(); + const url = `${openUrl}${lbryWebUrl}` + (urlParamsString === '' ? '' : `?${urlParamsString}`); + return url; +};