Allow video sharing with start timestamp

Closes #3122
This commit is contained in:
Jeffrey Fisher 2020-05-05 20:05:59 -07:00 committed by Sean Yesmunt
parent aa0a76a121
commit 8fe9cfafbc
8 changed files with 190 additions and 18 deletions

View file

@ -11,14 +11,16 @@ type Props = {
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
label?: string, label?: string,
claim: Claim, claim: Claim,
includeStartTime: boolean,
startTime: number,
}; };
export default function EmbedTextArea(props: Props) { 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 { claim_id: claimId, name } = claim;
const input = useRef(); const input = useRef();
const streamUrl = generateEmbedUrl(name, claimId); const streamUrl = generateEmbedUrl(name, claimId, includeStartTime, startTime);
let embedText = `<iframe width="560" height="315" src="${streamUrl}" allowfullscreen></iframe>`; let embedText = `<iframe width="560" height="315" src="${streamUrl}" allowfullscreen></iframe>`;
function copyToClipboard() { function copyToClipboard() {
@ -47,6 +49,7 @@ export default function EmbedTextArea(props: Props) {
value={embedText || ''} value={embedText || ''}
ref={input} ref={input}
onFocus={onFocus} onFocus={onFocus}
readOnly
/> />
<div className="section__actions"> <div className="section__actions">

View file

@ -2,12 +2,14 @@ import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux'; import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux';
import SocialShare from './view'; import SocialShare from './view';
import { selectUserInviteReferralCode, selectUser } from 'lbryinc'; import { selectUserInviteReferralCode, selectUser } from 'lbryinc';
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
referralCode: selectUserInviteReferralCode(state), referralCode: selectUserInviteReferralCode(state),
user: selectUser(state), user: selectUser(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
position: makeSelectContentPositionForUri(props.uri)(state),
}); });
export default connect(select)(SocialShare); export default connect(select)(SocialShare);

View file

@ -6,6 +6,9 @@ import CopyableText from 'component/copyableText';
import EmbedTextArea from 'component/embedTextArea'; import EmbedTextArea from 'component/embedTextArea';
import { generateDownloadUrl } from 'util/lbrytv'; import { generateDownloadUrl } from 'util/lbrytv';
import useIsMobile from 'effects/use-is-mobile'; 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 IOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform);
const SUPPORTS_SHARE_API = typeof navigator.share !== 'undefined'; const SUPPORTS_SHARE_API = typeof navigator.share !== 'undefined';
@ -16,29 +19,56 @@ type Props = {
webShareable: boolean, webShareable: boolean,
referralCode: string, referralCode: string,
user: any, user: any,
position: number,
}; };
function SocialShare(props: Props) { 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 [showEmbed, setShowEmbed] = React.useState(false);
const [showExtra, setShowExtra] = 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(); 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) { if (!claim) {
return null; 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() { function handleWebShareClick() {
if (navigator.share) { if (navigator.share) {
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 ( return (
<React.Fragment> <React.Fragment>
<CopyableText label={__('LBRY Link')} copyable={openDotLbryDotComUrl} /> <CopyableText label={__('LBRY Link')} copyable={openDotLbryDotComUrl} />
<div className="section__start-at">
<FormField
type="checkbox"
name="share_start_at_checkbox"
onChange={() => handleTimeCheckboxChange(!includeStartTime)}
checked={includeStartTime}
label={__('Start at')}
/>
<FormField
type="text"
name="share_start_at"
value={startTime}
disabled={!includeStartTime}
onChange={event => handleTimeChange(event.target.value)}
/>
</div>
<div className="section__actions"> <div className="section__actions">
<Button <Button
className="share" className="share"
@ -120,7 +195,14 @@ function SocialShare(props: Props) {
<Button icon={ICONS.SHARE} button="primary" label={__('Share via...')} onClick={handleWebShareClick} /> <Button icon={ICONS.SHARE} button="primary" label={__('Share via...')} onClick={handleWebShareClick} />
</div> </div>
)} )}
{showEmbed && <EmbedTextArea label={__('Embedded')} claim={claim} />} {showEmbed && (
<EmbedTextArea
label={__('Embedded')}
claim={claim}
includeStartTime={includeStartTime}
startTime={startTimeSeconds}
/>
)}
{showExtra && ( {showExtra && (
<div className="section"> <div className="section">
<CopyableText label={__('LBRY URL')} copyable={`lbry://${lbryUrl}`} /> <CopyableText label={__('LBRY URL')} copyable={`lbry://${lbryUrl}`} />

View file

@ -138,3 +138,15 @@
.section__actions--no-margin { .section__actions--no-margin {
margin-top: 0; margin-top: 0;
} }
.section__start-at {
display: flex;
margin-top: var(--spacing-large);
fieldset-section {
width: 6em;
margin-top: 0;
}
.checkbox {
margin-right: 10px;
}
}

View file

@ -6,8 +6,9 @@ function generateStreamUrl(claimName, claimId) {
return `${LBRY_TV_STREAMING_API}/content/claims/${claimName}/${claimId}/stream`; return `${LBRY_TV_STREAMING_API}/content/claims/${claimName}/${claimId}/stream`;
} }
function generateEmbedUrl(claimName, claimId) { function generateEmbedUrl(claimName, claimId, includeStartTime, startTime) {
return `${URL}/$/embed/${claimName}/${claimId}`; const queryParam = includeStartTime ? `?t=${startTime}` : '';
return `${URL}/$/embed/${claimName}/${claimId}${queryParam}`;
} }
function generateDownloadUrl(claimName, claimId) { function generateDownloadUrl(claimName, claimId) {

34
ui/util/time.js Normal file
View file

@ -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;
}

View file

@ -70,3 +70,41 @@ exports.generateInitialUrl = hash => {
} }
return url; 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;
};