strip out livestreams

This commit is contained in:
zeppi 2021-10-19 00:49:51 -04:00 committed by jessopb
parent fca18c26d3
commit ab9f70930d
78 changed files with 126 additions and 3504 deletions

View file

@ -7,7 +7,6 @@ import { useHistory } from 'react-router-dom';
import Button from 'component/button'; import Button from 'component/button';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import LivestreamLink from 'component/livestreamLink';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import { DEBOUNCE_WAIT_DURATION_MS } from 'constants/search'; import { DEBOUNCE_WAIT_DURATION_MS } from 'constants/search';
import { lighthouse } from 'redux/actions/search'; import { lighthouse } from 'redux/actions/search';
@ -115,9 +114,6 @@ function ChannelContent(props: Props) {
{!fetching && Boolean(claimsInChannel) && !channelIsBlocked && !channelIsBlackListed && ( {!fetching && Boolean(claimsInChannel) && !channelIsBlocked && !channelIsBlackListed && (
<HiddenNsfwClaims uri={uri} /> <HiddenNsfwClaims uri={uri} />
)} )}
<LivestreamLink uri={uri} />
{!fetching && channelIsBlackListed && ( {!fetching && channelIsBlackListed && (
<section className="card card--section"> <section className="card card--section">
<p> <p>

View file

@ -19,11 +19,9 @@ type Props = {
mentionTerm: string, mentionTerm: string,
noTopSuggestion?: boolean, noTopSuggestion?: boolean,
showMature: boolean, showMature: boolean,
isLivestream: boolean,
creatorUri: string, creatorUri: string,
commentorUris: Array<string>, commentorUris: Array<string>,
subscriptionUris: Array<string>, subscriptionUris: Array<string>,
unresolvedCommentors: Array<string>,
unresolvedSubscriptions: Array<string>, unresolvedSubscriptions: Array<string>,
canonicalCreator: string, canonicalCreator: string,
canonicalCommentors: Array<string>, canonicalCommentors: Array<string>,
@ -34,10 +32,8 @@ type Props = {
export default function ChannelMentionSuggestions(props: Props) { export default function ChannelMentionSuggestions(props: Props) {
const { const {
unresolvedCommentors,
unresolvedSubscriptions, unresolvedSubscriptions,
canonicalCreator, canonicalCreator,
isLivestream,
creatorUri, creatorUri,
inputRef, inputRef,
showMature, showMature,
@ -184,11 +180,6 @@ export default function ChannelMentionSuggestions(props: Props) {
} }
}, [doResolveUris, stringifiedResults]); }, [doResolveUris, stringifiedResults]);
// Only resolve commentors on Livestreams when actually mentioning/looking for it
React.useEffect(() => {
if (isLivestream && unresolvedCommentors && mentionTerm) doResolveUris(unresolvedCommentors);
}, [doResolveUris, isLivestream, mentionTerm, unresolvedCommentors]);
// Only resolve the subscriptions that match the mention term, instead of all // Only resolve the subscriptions that match the mention term, instead of all
React.useEffect(() => { React.useEffect(() => {
if (isTyping) return; if (isTyping) return;

View file

@ -7,7 +7,6 @@ import {
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectReflectingClaimForUri, makeSelectReflectingClaimForUri,
makeSelectClaimWasPurchased, makeSelectClaimWasPurchased,
makeSelectClaimIsStreamPlaceholder,
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectDateForUri, makeSelectDateForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
@ -23,7 +22,6 @@ import { doCollectionEdit } from 'redux/actions/collections';
import { doFileGet } from 'redux/actions/file'; import { doFileGet } from 'redux/actions/file';
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
@ -58,8 +56,6 @@ const select = (state, props) => {
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state), isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state), streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state), wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state),
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state), isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state), collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state),
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state), collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state),

View file

@ -10,7 +10,6 @@ import { formatLbryUrlForWeb } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
import FileThumbnail from 'component/fileThumbnail'; import FileThumbnail from 'component/fileThumbnail';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import PreviewOverlayProperties from 'component/previewOverlayProperties';
import ClaimTags from 'component/claimTags'; import ClaimTags from 'component/claimTags';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import ChannelThumbnail from 'component/channelThumbnail'; import ChannelThumbnail from 'component/channelThumbnail';
@ -26,7 +25,6 @@ import ClaimMenuList from 'component/claimMenuList';
import ClaimPreviewLoading from './claim-preview-loading'; import ClaimPreviewLoading from './claim-preview-loading';
import ClaimPreviewHidden from './claim-preview-no-mature'; import ClaimPreviewHidden from './claim-preview-no-mature';
import ClaimPreviewNoContent from './claim-preview-no-content'; import ClaimPreviewNoContent from './claim-preview-no-content';
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import Button from 'component/button'; import Button from 'component/button';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
@ -78,8 +76,6 @@ type Props = {
hideRepostLabel?: boolean, hideRepostLabel?: boolean,
repostUrl?: string, repostUrl?: string,
hideMenu?: boolean, hideMenu?: boolean,
isLivestream?: boolean,
isLivestreamActive: boolean,
collectionId?: string, collectionId?: string,
editCollection: (string, CollectionEditParams) => void, editCollection: (string, CollectionEditParams) => void,
isCollectionMine: boolean, isCollectionMine: boolean,
@ -144,8 +140,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
renderActions, renderActions,
hideMenu = false, hideMenu = false,
// repostUrl, // repostUrl,
isLivestream, // need both? CHECK
isLivestreamActive,
collectionId, collectionId,
collectionIndex, collectionIndex,
editCollection, editCollection,
@ -296,7 +290,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
} }
}, [isValid, uri, isResolvingUri, shouldFetch, resolveUri]); }, [isValid, uri, isResolvingUri, shouldFetch, resolveUri]);
if ((shouldHide && !showNullPlaceholder) || (isLivestream && !ENABLE_NO_SOURCE_CLAIMS)) { if (shouldHide && !showNullPlaceholder) {
return null; return null;
} }
@ -329,11 +323,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
return null; return null;
} }
let liveProperty = null;
if (isLivestreamActive === true) {
liveProperty = (claim) => <>LIVE</>;
}
return ( return (
<WrapperElement <WrapperElement
ref={ref} ref={ref}
@ -343,7 +332,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
'claim-preview__wrapper--channel': isChannelUri && type !== 'inline', 'claim-preview__wrapper--channel': isChannelUri && type !== 'inline',
'claim-preview__wrapper--inline': type === 'inline', 'claim-preview__wrapper--inline': type === 'inline',
'claim-preview__wrapper--small': type === 'small', 'claim-preview__wrapper--small': type === 'small',
'claim-preview__live': isLivestreamActive,
'claim-preview__active': active, 'claim-preview__active': active,
})} })}
> >
@ -380,11 +368,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
)} )}
</div> </div>
{/* @endif */} {/* @endif */}
{(!isLivestream || isLivestreamActive) && (
<div className="claim-preview__file-property-overlay">
<PreviewOverlayProperties uri={uri} small={type === 'small'} properties={liveProperty} />
</div>
)}
</FileThumbnail> </FileThumbnail>
</NavLink> </NavLink>
) : ( ) : (

View file

@ -1,10 +1,6 @@
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { makeSelectClaimForUri, makeSelectClaimIsPending } from 'redux/selectors/claims';
makeSelectClaimForUri,
makeSelectClaimIsPending,
makeSelectClaimIsStreamPlaceholder,
} from 'redux/selectors/claims';
import { doClearPublish, doPrepareEdit } from 'redux/actions/publish'; import { doClearPublish, doPrepareEdit } from 'redux/actions/publish';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import ClaimPreviewSubtitle from './view'; import ClaimPreviewSubtitle from './view';
@ -12,7 +8,6 @@ import ClaimPreviewSubtitle from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
pending: makeSelectClaimIsPending(props.uri)(state), pending: makeSelectClaimIsPending(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({

View file

@ -1,5 +1,4 @@
// @flow // @flow
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import React from 'react'; import React from 'react';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import DateTime from 'component/dateTime'; import DateTime from 'component/dateTime';
@ -13,12 +12,11 @@ type Props = {
pending?: boolean, pending?: boolean,
type: string, type: string,
beginPublish: (?string) => void, beginPublish: (?string) => void,
isLivestream: boolean,
}; };
// previews used in channel overview and homepage (and other places?) // previews used in channel overview and homepage (and other places?)
function ClaimPreviewSubtitle(props: Props) { function ClaimPreviewSubtitle(props: Props) {
const { pending, uri, claim, type, beginPublish, isLivestream } = props; const { pending, uri, claim, type, beginPublish } = props;
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
let isChannel; let isChannel;
@ -38,15 +36,12 @@ function ClaimPreviewSubtitle(props: Props) {
type !== 'inline' && type !== 'inline' &&
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`} `${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
{!isChannel && {!isChannel && (
(isLivestream && ENABLE_NO_SOURCE_CLAIMS ? ( <>
__('Livestream') <FileViewCountInline uri={uri} />
) : ( <DateTime timeAgo uri={uri} />
<> </>
<FileViewCountInline uri={uri} isLivestream={isLivestream} /> )}
<DateTime timeAgo uri={uri} />
</>
))}
</> </>
)} )}
</React.Fragment> </React.Fragment>

View file

@ -6,14 +6,12 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectClaimIsStreamPlaceholder,
makeSelectDateForUri, makeSelectDateForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { doFileGet } from 'redux/actions/file'; import { doFileGet } from 'redux/actions/file';
import { doResolveUri } from 'redux/actions/claims'; import { doResolveUri } from 'redux/actions/claims';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import ClaimPreviewTile from './view'; import ClaimPreviewTile from './view';
import formatMediaDuration from 'util/formatMediaDuration'; import formatMediaDuration from 'util/formatMediaDuration';
@ -36,8 +34,6 @@ const select = (state, props) => {
blockedChannelUris: selectMutedChannels(state), blockedChannelUris: selectMutedChannels(state),
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state), isMature: makeSelectClaimIsNsfw(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
isLivestreamActive: makeSelectIsActiveLivestream(props.uri)(state),
viewCount: makeSelectViewCountForUri(props.uri)(state), viewCount: makeSelectViewCountForUri(props.uri)(state),
}; };
}; };

View file

@ -49,10 +49,7 @@ type Props = {
showHiddenByUser?: boolean, showHiddenByUser?: boolean,
properties?: (Claim) => void, properties?: (Claim) => void,
collectionId?: string, collectionId?: string,
showNoSourceClaims?: boolean,
isLivestream: boolean,
viewCount: string, viewCount: string,
isLivestreamActive: boolean,
}; };
// preview image cards used in related video functionality, channel overview page and homepage // preview image cards used in related video functionality, channel overview page and homepage
@ -76,9 +73,6 @@ function ClaimPreviewTile(props: Props) {
showMature, showMature,
showHiddenByUser, showHiddenByUser,
properties, properties,
showNoSourceClaims,
isLivestream,
isLivestreamActive,
collectionId, collectionId,
mediaDuration, mediaDuration,
viewCount, viewCount,
@ -175,13 +169,13 @@ function ClaimPreviewTile(props: Props) {
shouldHide = blockedChannelUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url)); shouldHide = blockedChannelUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
} }
if (shouldHide || (isLivestream && !showNoSourceClaims)) { if (shouldHide) {
return null; return null;
} }
const isChannelPage = window.location.pathname.startsWith('/@'); const isChannelPage = window.location.pathname.startsWith('/@');
const shouldShowViewCount = !(!viewCount || (claim && claim.repost_url) || isLivestream || !isChannelPage); const shouldShowViewCount = !(!viewCount || (claim && claim.repost_url) || !isChannelPage);
if (placeholder || (!claim && isResolvingUri)) { if (placeholder || (!claim && isResolvingUri)) {
return ( return (
@ -202,16 +196,12 @@ function ClaimPreviewTile(props: Props) {
} }
let liveProperty = null; let liveProperty = null;
if (isLivestreamActive === true) {
liveProperty = (claim) => <>LIVE</>;
}
return ( return (
<li <li
onClick={handleClick} onClick={handleClick}
className={classnames('card claim-preview--tile', { className={classnames('card claim-preview--tile', {
'claim-preview__wrapper--channel': isChannel, 'claim-preview__wrapper--channel': isChannel,
'claim-preview__live': isLivestreamActive,
})} })}
> >
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden> <NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
@ -273,7 +263,7 @@ function ClaimPreviewTile(props: Props) {
<div className="claim-tile__about"> <div className="claim-tile__about">
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
<div className="claim-tile__about--counts"> <div className="claim-tile__about--counts">
<FileViewCountInline uri={uri} isLivestream={isLivestream} /> <FileViewCountInline uri={uri} />
<DateTime timeAgo uri={uri} /> <DateTime timeAgo uri={uri} />
</div> </div>
</div> </div>

View file

@ -1,10 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import { makeSelectClaimForUri } from 'redux/selectors/claims';
import FileType from './view'; import FileType from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
}); });
export default connect(select)(FileType); export default connect(select)(FileType);

View file

@ -26,18 +26,7 @@ const select = (state, props) => ({
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) => createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
dispatch( dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, txid, payment_intent_id, environment)),
doCommentCreate(
comment,
claimId,
parentId,
ownProps.uri,
ownProps.livestream,
txid,
payment_intent_id,
environment
)
),
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
doToast: (options) => dispatch(doToast(options)), doToast: (options) => dispatch(doToast(options)),
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)), doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),

View file

@ -1,5 +1,5 @@
// @flow // @flow
import { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
@ -40,7 +40,6 @@ type Props = {
activeChannel: string, activeChannel: string,
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
bottom: boolean, bottom: boolean,
livestream?: boolean,
embed?: boolean, embed?: boolean,
claimIsMine: boolean, claimIsMine: boolean,
supportDisabled: boolean, supportDisabled: boolean,
@ -68,8 +67,6 @@ export function CommentCreate(props: Props) {
parentId, parentId,
activeChannelClaim, activeChannelClaim,
bottom, bottom,
livestream,
embed,
claimIsMine, claimIsMine,
settingsByChannelId, settingsByChannelId,
supportDisabled, supportDisabled,
@ -179,7 +176,6 @@ export function CommentCreate(props: Props) {
let newMentionValue = mentionValue.replace('lbry://', ''); let newMentionValue = mentionValue.replace('lbry://', '');
if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':'); if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':');
if (livestream && key !== KEYCODES.TAB) setPauseQuickSend(true);
setCommentValue( setCommentValue(
commentValue.substring(0, selectedMentionIndex) + commentValue.substring(0, selectedMentionIndex) +
`${newMentionValue}` + `${newMentionValue}` +
@ -190,7 +186,7 @@ export function CommentCreate(props: Props) {
} }
function altEnterListener(e: SyntheticKeyboardEvent<*>) { function altEnterListener(e: SyntheticKeyboardEvent<*>) {
if ((livestream || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) { if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
e.preventDefault(); e.preventDefault();
buttonRef.current.click(); buttonRef.current.click();
} }
@ -442,17 +438,8 @@ export function CommentCreate(props: Props) {
<div <div
role="button" role="button"
onClick={() => { onClick={() => {
if (embed) {
window.open(`https://odysee.com/$/${PAGES.AUTH}?redirect=/$/${PAGES.LIVESTREAM}`);
return;
}
const pathPlusRedirect = `/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`; const pathPlusRedirect = `/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`;
if (livestream) { push(pathPlusRedirect);
window.open(pathPlusRedirect);
} else {
push(pathPlusRedirect);
}
}} }}
> >
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} /> <FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
@ -518,7 +505,6 @@ export function CommentCreate(props: Props) {
{!advancedEditor && ( {!advancedEditor && (
<ChannelMentionSuggestions <ChannelMentionSuggestions
uri={uri} uri={uri}
isLivestream={livestream}
inputRef={formFieldInputRef} inputRef={formFieldInputRef}
mentionTerm={channelMention} mentionTerm={channelMention}
creatorUri={channelUri} creatorUri={channelUri}
@ -533,9 +519,7 @@ export function CommentCreate(props: Props) {
className={isReply ? 'content_reply' : 'content_comment'} className={isReply ? 'content_reply' : 'content_comment'}
label={ label={
<span className="comment-new__label-wrapper"> <span className="comment-new__label-wrapper">
{!livestream && ( <div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
)}
<SelectChannel tiny /> <SelectChannel tiny />
</span> </span>
} }
@ -548,7 +532,7 @@ export function CommentCreate(props: Props) {
charCount={charCount} charCount={charCount}
onChange={handleCommentChange} onChange={handleCommentChange}
autoFocus={isReply} autoFocus={isReply}
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT} textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
/> />
{isSupportComment && ( {isSupportComment && (
<WalletTipAmountSelector <WalletTipAmountSelector

View file

@ -1767,532 +1767,6 @@ export const icons = {
</g> </g>
</svg> </svg>
), ),
[ICONS.LIVESTREAM]: (props: CustomProps) => (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 36 36"
width={props.size || '18'}
height={props.size || '16'}
className={props.className}
>
<g id="XMLID_505_">
<linearGradient
id="XMLID_420_"
gradientUnits="userSpaceOnUse"
x1="-519.065"
y1="1525.4059"
x2="-508.6628"
y2="1525.4059"
>
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.3866" stopColor="#FF31BD" />
<stop offset="0.6245" stopColor="#8E31BD" />
<stop offset="0.7758" stopColor="#6E8EDE" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<circle
id="XMLID_508_"
fill="none"
stroke="url(XMLID_420_)"
strokeWidth="2.4678"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
cx="-513.9"
cy="1525.4"
r="4"
/>
<path
id="XMLID_507_"
fill="#FF7B5B"
d="M-521,1518.3c-1.8,1.8-2.9,4.3-2.9,7.1c0,2.6,1,4.9,2.5,6.7L-521,1518.3z"
/>
<path
id="XMLID_506_"
fill="#FF7B5B"
d="M-506.9,1532.1c1.8-1.8,2.9-4.3,2.9-7.1c0-2.6-1-4.9-2.5-6.7L-506.9,1532.1z"
/>
</g>
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" />
{/* }//fill="#FFFFFF" */}
<linearGradient
id="XMLID_421_"
gradientUnits="userSpaceOnUse"
x1="-1625.151"
y1="-2518.4661"
x2="-1596.6696"
y2="-2518.4661"
gradientTransform="matrix(-1 0 0 -1 -1589.489 -2500.4661)"
>
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<path
id="XMLID_124_"
fill="none"
stroke="url(#XMLID_421_)"
strokeWidth="2.94"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
d="M21.4,5.2L21.4,5.2c7.1,0,12.8,5.7,12.8,12.8v0c0,7.1-5.7,12.8-12.8,12.8H8.7V18C8.7,10.9,14.4,5.2,21.4,5.2z"
/>
<linearGradient id="XMLID_422_" gradientUnits="userSpaceOnUse" x1="18.041" y1="32.147" x2="38.7776" y2="-0.9289">
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<rect id="XMLID_123_" x="26.9" y="13.8" fill="url(#XMLID_422_)" stroke="none" width="2.8" height="3.8" />
<linearGradient
id="XMLID_423_"
gradientUnits="userSpaceOnUse"
x1="13.0856"
y1="29.0402"
x2="33.8223"
y2="-4.0356"
>
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<rect id="XMLID_122_" x="20" y="13.8" fill="url(#XMLID_422_)" stroke="none" width="2.8" height="3.8" />
<linearGradient id="XMLID_424_" gradientUnits="userSpaceOnUse" x1="0.338" y1="17.7555" x2="17.2654" y2="17.7555">
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<circle
id="XMLID_121_"
fill="none"
stroke="#6E8EDE"
strokeWidth="2.94"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
cx="8.8"
cy="17.8"
r="6"
/>
</svg>
),
[ICONS.LIVESTREAM_SOLID]: (props: CustomProps) => (
<svg
id="prefix__Layer_1"
xmlns="http://www.w3.org/2000/svg"
x={0}
y={0}
fill="none"
width={props.size || '18'}
height={props.size || '16'}
viewBox="0 0 36 36"
xmlSpace="preserve"
{...props}
>
<style>{'.prefix__st1{fill:#ff7b5b}.prefix__st3{fill:#79d1b6}'}</style>
<g id="prefix__XMLID_505_">
<linearGradient
id="prefix__XMLID_410_"
gradientUnits="userSpaceOnUse"
x1={-571.815}
y1={1525.406}
x2={-561.413}
y2={1525.406}
>
<stop offset={0.02} stopColor="#ffc200" />
<stop offset={0.387} stopColor="#ff31bd" />
<stop offset={0.625} stopColor="#8e31bd" />
<stop offset={0.776} stopColor="#6e8ede" />
<stop offset={1} stopColor="#57eaba" />
</linearGradient>
<circle
id="prefix__XMLID_508_"
cx={-566.6}
cy={1525.4}
r={4}
fill="none"
stroke="url(#prefix__XMLID_410_)"
strokeWidth={2.468}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path
id="prefix__XMLID_507_"
className="prefix__st1"
d="M-573.7 1518.3c-1.8 1.8-2.9 4.3-2.9 7.1 0 2.6 1 4.9 2.5 6.7l.4-13.8z"
/>
<path
id="prefix__XMLID_506_"
className="prefix__st1"
d="M-559.6 1532.1c1.8-1.8 2.9-4.3 2.9-7.1 0-2.6-1-4.9-2.5-6.7l-.4 13.8z"
/>
</g>
<path
id="prefix__XMLID_20_"
d="M21.4 5.2h0c7.1 0 12.8 5.7 12.8 12.8v0c0 7.1-5.7 12.8-12.8 12.8H8.7V18c0-7.1 5.7-12.8 12.7-12.8z"
fill="none"
stroke="#e729e1"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path id="prefix__XMLID_19_" className="prefix__st3" stroke="none" d="M26.9 13.8h2.8v3.8h-2.8z" />
<path id="prefix__XMLID_18_" className="prefix__st3" stroke="none" d="M20 13.8h2.8v3.8H20z" />
<circle
id="prefix__XMLID_17_"
cx={8.8}
cy={17.8}
r={6}
fill="none"
stroke="#ffa100"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
</svg>
),
[ICONS.LIVESTREAM_MONOCHROME]: (props: CustomProps) => (
<svg
id="prefix__Layer_1"
xmlns="http://www.w3.org/2000/svg"
x={0}
y={0}
fill="currentColor"
stroke="currentColor"
width={props.size || '18'}
height={props.size || '16'}
viewBox="0 0 36 36"
xmlSpace="preserve"
{...props}
>
<g id="prefix__XMLID_505_">
<linearGradient
id="prefix__XMLID_410_"
gradientUnits="userSpaceOnUse"
x1={-571.815}
y1={1525.406}
x2={-561.413}
y2={1525.406}
>
<stop offset={0.02} stopColor="#ffc200" />
<stop offset={0.387} stopColor="#ff31bd" />
<stop offset={0.625} stopColor="#8e31bd" />
<stop offset={0.776} stopColor="#6e8ede" />
<stop offset={1} stopColor="#57eaba" />
</linearGradient>
<circle
id="prefix__XMLID_508_"
cx={-566.6}
cy={1525.4}
r={4}
fill="none"
strokeWidth={2.468}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path id="prefix__XMLID_507_" d="M-573.7 1518.3c-1.8 1.8-2.9 4.3-2.9 7.1 0 2.6 1 4.9 2.5 6.7l.4-13.8z" />
<path id="prefix__XMLID_506_" d="M-559.6 1532.1c1.8-1.8 2.9-4.3 2.9-7.1 0-2.6-1-4.9-2.5-6.7l-.4 13.8z" />
</g>
<path
id="prefix__XMLID_20_"
d="M21.4 5.2h0c7.1 0 12.8 5.7 12.8 12.8v0c0 7.1-5.7 12.8-12.8 12.8H8.7V18c0-7.1 5.7-12.8 12.7-12.8z"
fill="none"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path id="prefix__XMLID_19_" d="M26.9 13.8h2.8v3.8h-2.8z" />
<path id="prefix__XMLID_18_" d="M20 13.8h2.8v3.8H20z" />
<circle
id="prefix__XMLID_17_"
cx={8.8}
cy={17.8}
r={6}
fill="none"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
</svg>
),
[ICONS.LIVESTREAM]: (props: CustomProps) => (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 36 36"
width={props.size || '18'}
height={props.size || '16'}
>
<g id="XMLID_505_">
<linearGradient
id="XMLID_420_"
gradientUnits="userSpaceOnUse"
x1="-519.065"
y1="1525.4059"
x2="-508.6628"
y2="1525.4059"
>
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.3866" stopColor="#FF31BD" />
<stop offset="0.6245" stopColor="#8E31BD" />
<stop offset="0.7758" stopColor="#6E8EDE" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<circle
id="XMLID_508_"
fill="none"
stroke="url(XMLID_420_)"
strokeWidth="2.4678"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
cx="-513.9"
cy="1525.4"
r="4"
/>
<path
id="XMLID_507_"
fill="#FF7B5B"
d="M-521,1518.3c-1.8,1.8-2.9,4.3-2.9,7.1c0,2.6,1,4.9,2.5,6.7L-521,1518.3z"
/>
<path
id="XMLID_506_"
fill="#FF7B5B"
d="M-506.9,1532.1c1.8-1.8,2.9-4.3,2.9-7.1c0-2.6-1-4.9-2.5-6.7L-506.9,1532.1z"
/>
</g>
<rect id="XMLID_125_" x="0" y="0" fill="none" width="36" height="36" stroke="none" />
{/* }//fill="#FFFFFF" */}
<linearGradient
id="XMLID_421_"
gradientUnits="userSpaceOnUse"
x1="-1625.151"
y1="-2518.4661"
x2="-1596.6696"
y2="-2518.4661"
gradientTransform="matrix(-1 0 0 -1 -1589.489 -2500.4661)"
>
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<path
id="XMLID_124_"
fill="none"
stroke="url(#XMLID_421_)"
strokeWidth="2.94"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
d="M21.4,5.2L21.4,5.2c7.1,0,12.8,5.7,12.8,12.8v0c0,7.1-5.7,12.8-12.8,12.8H8.7V18C8.7,10.9,14.4,5.2,21.4,5.2z"
/>
<linearGradient id="XMLID_422_" gradientUnits="userSpaceOnUse" x1="18.041" y1="32.147" x2="38.7776" y2="-0.9289">
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<rect id="XMLID_123_" x="26.9" y="13.8" fill="url(#XMLID_422_)" stroke="none" width="2.8" height="3.8" />
<linearGradient
id="XMLID_423_"
gradientUnits="userSpaceOnUse"
x1="13.0856"
y1="29.0402"
x2="33.8223"
y2="-4.0356"
>
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<rect id="XMLID_122_" x="20" y="13.8" fill="url(#XMLID_422_)" stroke="none" width="2.8" height="3.8" />
<linearGradient id="XMLID_424_" gradientUnits="userSpaceOnUse" x1="0.338" y1="17.7555" x2="17.2654" y2="17.7555">
<stop offset="1.970443e-002" stopColor="#FFC200" />
<stop offset="0.4731" stopColor="#FF31BD" />
<stop offset="0.6947" stopColor="#8E31BD" />
<stop offset="1" stopColor="#57EABA" />
</linearGradient>
<circle
id="XMLID_121_"
fill="none"
stroke="#6E8EDE"
strokeWidth="2.94"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
cx="8.8"
cy="17.8"
r="6"
/>
</svg>
),
[ICONS.LIVESTREAM_SOLID]: (props: CustomProps) => (
<svg
id="prefix__Layer_1"
xmlns="http://www.w3.org/2000/svg"
x={0}
y={0}
fill="none"
width={props.size || '18'}
height={props.size || '16'}
viewBox="0 0 36 36"
xmlSpace="preserve"
{...props}
>
<style>{'.prefix__st1{fill:#ff7b5b}.prefix__st3{fill:#79d1b6}'}</style>
<g id="prefix__XMLID_505_">
<linearGradient
id="prefix__XMLID_410_"
gradientUnits="userSpaceOnUse"
x1={-571.815}
y1={1525.406}
x2={-561.413}
y2={1525.406}
>
<stop offset={0.02} stopColor="#ffc200" />
<stop offset={0.387} stopColor="#ff31bd" />
<stop offset={0.625} stopColor="#8e31bd" />
<stop offset={0.776} stopColor="#6e8ede" />
<stop offset={1} stopColor="#57eaba" />
</linearGradient>
<circle
id="prefix__XMLID_508_"
cx={-566.6}
cy={1525.4}
r={4}
fill="none"
stroke="url(#prefix__XMLID_410_)"
strokeWidth={2.468}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path
id="prefix__XMLID_507_"
className="prefix__st1"
d="M-573.7 1518.3c-1.8 1.8-2.9 4.3-2.9 7.1 0 2.6 1 4.9 2.5 6.7l.4-13.8z"
/>
<path
id="prefix__XMLID_506_"
className="prefix__st1"
d="M-559.6 1532.1c1.8-1.8 2.9-4.3 2.9-7.1 0-2.6-1-4.9-2.5-6.7l-.4 13.8z"
/>
</g>
<path
id="prefix__XMLID_20_"
d="M21.4 5.2h0c7.1 0 12.8 5.7 12.8 12.8v0c0 7.1-5.7 12.8-12.8 12.8H8.7V18c0-7.1 5.7-12.8 12.7-12.8z"
fill="none"
stroke="#e729e1"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path id="prefix__XMLID_19_" className="prefix__st3" stroke="none" d="M26.9 13.8h2.8v3.8h-2.8z" />
<path id="prefix__XMLID_18_" className="prefix__st3" stroke="none" d="M20 13.8h2.8v3.8H20z" />
<circle
id="prefix__XMLID_17_"
cx={8.8}
cy={17.8}
r={6}
fill="none"
stroke="#ffa100"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
</svg>
),
[ICONS.LIVESTREAM_MONOCHROME]: (props: CustomProps) => (
<svg
id="prefix__Layer_1"
xmlns="http://www.w3.org/2000/svg"
x={0}
y={0}
fill="currentColor"
stroke="currentColor"
width={props.size || '18'}
height={props.size || '16'}
viewBox="0 0 36 36"
xmlSpace="preserve"
{...props}
>
<g id="prefix__XMLID_505_">
<linearGradient
id="prefix__XMLID_410_"
gradientUnits="userSpaceOnUse"
x1={-571.815}
y1={1525.406}
x2={-561.413}
y2={1525.406}
>
<stop offset={0.02} stopColor="#ffc200" />
<stop offset={0.387} stopColor="#ff31bd" />
<stop offset={0.625} stopColor="#8e31bd" />
<stop offset={0.776} stopColor="#6e8ede" />
<stop offset={1} stopColor="#57eaba" />
</linearGradient>
<circle
id="prefix__XMLID_508_"
cx={-566.6}
cy={1525.4}
r={4}
fill="none"
strokeWidth={2.468}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path id="prefix__XMLID_507_" d="M-573.7 1518.3c-1.8 1.8-2.9 4.3-2.9 7.1 0 2.6 1 4.9 2.5 6.7l.4-13.8z" />
<path id="prefix__XMLID_506_" d="M-559.6 1532.1c1.8-1.8 2.9-4.3 2.9-7.1 0-2.6-1-4.9-2.5-6.7l-.4 13.8z" />
</g>
<path
id="prefix__XMLID_20_"
d="M21.4 5.2h0c7.1 0 12.8 5.7 12.8 12.8v0c0 7.1-5.7 12.8-12.8 12.8H8.7V18c0-7.1 5.7-12.8 12.7-12.8z"
fill="none"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
<path id="prefix__XMLID_19_" d="M26.9 13.8h2.8v3.8h-2.8z" />
<path id="prefix__XMLID_18_" d="M20 13.8h2.8v3.8H20z" />
<circle
id="prefix__XMLID_17_"
cx={8.8}
cy={17.8}
r={6}
fill="none"
strokeWidth={2.94}
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
/>
</svg>
),
[ICONS.STACK]: (props: CustomProps) => ( [ICONS.STACK]: (props: CustomProps) => (
<svg <svg
{...props} {...props}

View file

@ -3,7 +3,6 @@ import {
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectClaimForUri, makeSelectClaimForUri,
selectMyChannelClaims, selectMyChannelClaims,
makeSelectClaimIsStreamPlaceholder,
makeSelectTagInClaimOrChannelForUri, makeSelectTagInClaimOrChannelForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri, makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectStreamingUrlForUri, makeSelectFileInfoForUri } from 'redux/selectors/file_info';
@ -24,7 +23,6 @@ const select = (state, props) => ({
renderMode: makeSelectFileRenderModeForUri(props.uri)(state), renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state),
myChannels: selectMyChannelClaims(state), myChannels: selectMyChannelClaims(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
reactionsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state), reactionsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state), streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
}); });

View file

@ -30,7 +30,6 @@ type Props = {
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
clearPlayingUri: () => void, clearPlayingUri: () => void,
hideRepost?: boolean, hideRepost?: boolean,
isLivestreamClaim: boolean,
reactionsDisabled: boolean, reactionsDisabled: boolean,
download: (string) => void, download: (string) => void,
streamingUrl: ?string, streamingUrl: ?string,
@ -50,7 +49,6 @@ function FileActions(props: Props) {
clearPlayingUri, clearPlayingUri,
doToast, doToast,
hideRepost, hideRepost,
isLivestreamClaim,
reactionsDisabled, reactionsDisabled,
} = props; } = props;
const { const {
@ -96,7 +94,7 @@ function FileActions(props: Props) {
const lhsSection = ( const lhsSection = (
<> <>
{ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} livestream={isLivestreamClaim} />} {ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} />}
<ClaimSupportButton uri={uri} fileAction /> <ClaimSupportButton uri={uri} fileAction />
<ClaimCollectionAddButton uri={uri} fileAction /> <ClaimCollectionAddButton uri={uri} fileAction />
{!hideRepost && ( {!hideRepost && (
@ -124,14 +122,12 @@ function FileActions(props: Props) {
const rhsSection = ( const rhsSection = (
<> <>
{/* @if TARGET='app' */}
<FileDownloadLink uri={uri} /> <FileDownloadLink uri={uri} />
{/* @endif */}
{claimIsMine && ( {claimIsMine && (
<Button <Button
className="button--file-action" className="button--file-action"
icon={ICONS.EDIT} icon={ICONS.EDIT}
label={isLivestreamClaim ? __('Update') : __('Edit')} label={__('Edit')}
navigate={`/$/${PAGES.UPLOAD}`} navigate={`/$/${PAGES.UPLOAD}`}
onClick={() => { onClick={() => {
prepareEdit(claim, editUri, fileInfo); prepareEdit(claim, editUri, fileInfo);
@ -147,7 +143,7 @@ function FileActions(props: Props) {
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })} onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
/> />
)} )}
{(!isLivestreamClaim || !claimIsMine) && ( {!claimIsMine && (
<Menu> <Menu>
<MenuButton <MenuButton
className="button--file-action" className="button--file-action"
@ -159,17 +155,15 @@ function FileActions(props: Props) {
<Icon size={20} icon={ICONS.MORE} /> <Icon size={20} icon={ICONS.MORE} />
</MenuButton> </MenuButton>
<MenuList className="menu__list"> <MenuList className="menu__list">
{!claimIsMine && ( <MenuItem
<MenuItem className="comment__menu-option"
className="comment__menu-option" onSelect={() => push(`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`)}
onSelect={() => push(`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`)} >
> <div className="menu__link">
<div className="menu__link"> <Icon aria-hidden icon={ICONS.REPORT} />
<Icon aria-hidden icon={ICONS.REPORT} /> {__('Report content')}
{__('Report content')} </div>
</div> </MenuItem>
</MenuItem>
)}
</MenuList> </MenuList>
</Menu> </Menu>
)} )}

View file

@ -16,7 +16,6 @@ type Props = {
likeCount: number, likeCount: number,
dislikeCount: number, dislikeCount: number,
myReaction: ?string, myReaction: ?string,
livestream?: boolean,
}; };
function FileReactions(props: Props) { function FileReactions(props: Props) {
@ -29,7 +28,6 @@ function FileReactions(props: Props) {
myReaction, myReaction,
likeCount, likeCount,
dislikeCount, dislikeCount,
livestream,
} = props; } = props;
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
@ -42,21 +40,10 @@ function FileReactions(props: Props) {
doFetchReactions(claimId); doFetchReactions(claimId);
} }
let fetchInterval;
if (claimId) { if (claimId) {
fetchReactions(); fetchReactions();
if (livestream) {
fetchInterval = setInterval(fetchReactions, 45000);
}
} }
}, [claimId, doFetchReactions]);
return () => {
if (fetchInterval) {
clearInterval(fetchInterval);
}
};
}, [claimId, doFetchReactions, livestream]);
return ( return (
<> <>
@ -98,7 +85,9 @@ function FileReactions(props: Props) {
requiresAuth={IS_WEB} requiresAuth={IS_WEB}
authSrc={'filereaction_dislike'} authSrc={'filereaction_dislike'}
title={__('I dislike this')} title={__('I dislike this')}
className={classnames('button--file-action', { 'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE })} className={classnames('button--file-action', {
'button--file-action-active': myReaction === REACTION_TYPES.DISLIKE,
})}
label={<>{formatNumberWithCommas(dislikeCount, 0)}</>} label={<>{formatNumberWithCommas(dislikeCount, 0)}</>}
iconSize={18} iconSize={18}
icon={dislikeIcon} icon={dislikeIcon}

View file

@ -1,28 +1,22 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import DateTime from 'component/dateTime';
import FileViewCount from 'component/fileViewCount'; import FileViewCount from 'component/fileViewCount';
import FileActions from 'component/fileActions'; import FileActions from 'component/fileActions';
type Props = { type Props = {
uri: string, uri: string,
livestream?: boolean,
activeViewers?: number,
isLive?: boolean,
}; };
function FileSubtitle(props: Props) { function FileSubtitle(props: Props) {
const { uri, livestream = false, activeViewers, isLive = false } = props; const { uri } = props;
return ( return (
<div className="media__subtitle--between"> <div className="media__subtitle--between">
<div className="file__viewdate"> <div className="file__viewdate">
{livestream ? <span>{__('Right now')}</span> : <DateTime uri={uri} show={DateTime.SHOW_DATE} />} <FileViewCount uri={uri} />
<FileViewCount uri={uri} livestream={livestream} activeViewers={activeViewers} isLive={isLive} />
</div> </div>
<FileActions uri={uri} hideRepost={livestream} livestream={livestream} /> <FileActions uri={uri} />
</div> </div>
); );
} }

View file

@ -2,18 +2,15 @@ import { connect } from 'react-redux';
import { doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc'; import { doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc';
import { makeSelectTitleForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectTitleForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content'; import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
import { makeSelectViewersForId } from 'redux/selectors/livestream';
import FileTitleSection from './view'; import FileTitleSection from './view';
const select = (state, props) => { const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri)(state); const claim = makeSelectClaimForUri(props.uri)(state);
const viewers = claim && makeSelectViewersForId(claim.claim_id)(state);
const channelClaimId = claim && claim.signing_channel ? claim.signing_channel.claim_id : undefined; const channelClaimId = claim && claim.signing_channel ? claim.signing_channel.claim_id : undefined;
const channelUri = claim && claim.signing_channel ? claim.signing_channel.canonical_url : undefined; const channelUri = claim && claim.signing_channel ? claim.signing_channel.canonical_url : undefined;
const subCount = channelUri && makeSelectSubCountForUri(channelUri)(state); const subCount = channelUri && makeSelectSubCountForUri(channelUri)(state);
return { return {
viewers,
isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state), isInsufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
channelClaimId, channelClaimId,

View file

@ -20,27 +20,13 @@ type Props = {
title: string, title: string,
nsfw: boolean, nsfw: boolean,
isNsfwBlocked: boolean, isNsfwBlocked: boolean,
livestream?: boolean,
isLive?: boolean,
viewers?: number,
subCount: number, subCount: number,
channelClaimId?: string, channelClaimId?: string,
fetchSubCount: (string) => void, fetchSubCount: (string) => void,
}; };
function FileTitleSection(props: Props) { function FileTitleSection(props: Props) {
const { const { title, uri, nsfw, isNsfwBlocked, subCount, channelClaimId, fetchSubCount } = props;
title,
uri,
nsfw,
isNsfwBlocked,
livestream = false,
isLive = false,
viewers,
subCount,
channelClaimId,
fetchSubCount,
} = props;
const [hasAcknowledgedSec, setHasAcknowledgedSec] = usePersistedState('sec-nag', false); const [hasAcknowledgedSec, setHasAcknowledgedSec] = usePersistedState('sec-nag', false);
React.useEffect(() => { React.useEffect(() => {
@ -78,7 +64,7 @@ function FileTitleSection(props: Props) {
body={ body={
<React.Fragment> <React.Fragment>
<ClaimInsufficientCredits uri={uri} /> <ClaimInsufficientCredits uri={uri} />
<FileSubtitle uri={uri} isLive={isLive} livestream={livestream} activeViewers={viewers} /> <FileSubtitle uri={uri} />
</React.Fragment> </React.Fragment>
} }
actions={ actions={

View file

@ -1,11 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectMediaTypeForUri } from 'redux/selectors/file_info'; import { makeSelectMediaTypeForUri } from 'redux/selectors/file_info';
import { makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims';
import FileType from './view'; import FileType from './view';
const select = (state, props) => ({ const select = (state, props) => ({
mediaType: makeSelectMediaTypeForUri(props.uri)(state), mediaType: makeSelectMediaTypeForUri(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
}); });
export default connect(select)(FileType); export default connect(select)(FileType);

View file

@ -7,19 +7,18 @@ import * as COL from 'constants/collections';
type Props = { type Props = {
uri: string, uri: string,
mediaType: string, mediaType: string,
isLivestream: boolean,
small: boolean, small: boolean,
}; };
function FileType(props: Props) { function FileType(props: Props) {
const { mediaType, isLivestream, small } = props; const { mediaType, small } = props;
const size = small ? COL.ICON_SIZE : undefined; const size = small ? COL.ICON_SIZE : undefined;
if (mediaType === 'image') { if (mediaType === 'image') {
return <Icon size={size} icon={ICONS.IMAGE} />; return <Icon size={size} icon={ICONS.IMAGE} />;
} else if (mediaType === 'audio') { } else if (mediaType === 'audio') {
return <Icon size={size} icon={ICONS.AUDIO} />; return <Icon size={size} icon={ICONS.AUDIO} />;
} else if (mediaType === 'video' || isLivestream) { } else if (mediaType === 'video') {
return <Icon size={size} icon={ICONS.VIDEO} />; return <Icon size={size} icon={ICONS.VIDEO} />;
} else if (mediaType === 'text') { } else if (mediaType === 'text') {
return <Icon size={size} icon={ICONS.TEXT} />; return <Icon size={size} icon={ICONS.TEXT} />;

View file

@ -8,24 +8,12 @@ type Props = {
fetchingViewCount: boolean, fetchingViewCount: boolean,
uri: string, uri: string,
viewCount: string, viewCount: string,
livestream?: boolean,
activeViewers?: number,
isLive?: boolean,
doAnalyticsView: (string) => void,
}; };
function FileViewCount(props: Props) { function FileViewCount(props: Props) {
const { claim, uri, fetchViewCount, viewCount, livestream, activeViewers, doAnalyticsView } = props; const { claim, uri, fetchViewCount, viewCount } = props;
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
React.useEffect(() => {
if (livestream) {
// Regular claims will call the file/view event when a user actually watches the claim
// This can be removed when we get rid of the livestream iframe
doAnalyticsView(uri);
}
}, [livestream, doAnalyticsView, uri]);
React.useEffect(() => { React.useEffect(() => {
if (claimId) { if (claimId) {
fetchViewCount(claimId); fetchViewCount(claimId);
@ -36,9 +24,7 @@ function FileViewCount(props: Props) {
return ( return (
<span className="media__subtitle--centered"> <span className="media__subtitle--centered">
{!livestream && {viewCount !== 1 ? __('%view_count% views', { view_count: formattedViewCount }) : __('1 view')}
activeViewers === undefined &&
(viewCount !== 1 ? __('%view_count% views', { view_count: formattedViewCount }) : __('1 view'))}
{<HelpLink href="https://lbry.com/faq/views" />} {<HelpLink href="https://lbry.com/faq/views" />}
</span> </span>
); );

View file

@ -4,7 +4,6 @@ import 'scss/component/_view_count.scss';
type Props = { type Props = {
uri: string, uri: string,
isLivestream?: boolean,
// --- select --- // --- select ---
claim: ?StreamClaim, claim: ?StreamClaim,
viewCount: string, viewCount: string,
@ -12,7 +11,7 @@ type Props = {
}; };
export default function FileViewCountInline(props: Props) { export default function FileViewCountInline(props: Props) {
const { isLivestream, claim, viewCount, lang } = props; const { claim, viewCount, lang } = props;
let formattedViewCount; let formattedViewCount;
try { try {
@ -30,8 +29,8 @@ export default function FileViewCountInline(props: Props) {
// clean up (only one place edit/remove). // clean up (only one place edit/remove).
const isChannelPage = window.location.pathname.startsWith('/@'); const isChannelPage = window.location.pathname.startsWith('/@');
// dont show if no view count, if it's a repost, a livestream or isn't a channel page // dont show if no view count, if it's a repost or isn't a channel page
if (!viewCount || (claim && claim.repost_url) || isLivestream || !isChannelPage) { if (!viewCount || (claim && claim.repost_url) || !isChannelPage) {
// (1) Currently, makeSelectViewCountForUri doesn't differentiate between // (1) Currently, makeSelectViewCountForUri doesn't differentiate between
// un-fetched view-count vs zero view-count. But since it's probably not // un-fetched view-count vs zero view-count. But since it's probably not
// ideal to highlight that a view has 0 count, let's just not show anything. // ideal to highlight that a view has 0 count, let's just not show anything.

View file

@ -9,7 +9,7 @@ import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { doSignOut, doOpenModal } from 'redux/actions/app'; import { doSignOut, doOpenModal } from 'redux/actions/app';
import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectLanguage } from 'redux/selectors/settings';
import { selectHasNavigated, selectActiveChannelClaim, selectActiveChannelStakedLevel } from 'redux/selectors/app'; import { selectHasNavigated, selectActiveChannelClaim } from 'redux/selectors/app';
import Header from './view'; import Header from './view';
const select = (state) => ({ const select = (state) => ({
@ -27,7 +27,6 @@ const select = (state) => ({
hasNavigated: selectHasNavigated(state), hasNavigated: selectHasNavigated(state),
user: selectUser(state), user: selectUser(state),
activeChannelClaim: selectActiveChannelClaim(state), activeChannelClaim: selectActiveChannelClaim(state),
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({

View file

@ -1,5 +1,5 @@
// @flow // @flow
import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM, ENABLE_UI_NOTIFICATIONS } from 'config'; import { ENABLE_UI_NOTIFICATIONS } from 'config';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
@ -63,7 +63,6 @@ type Props = {
isAbsoluteSideNavHidden: boolean, isAbsoluteSideNavHidden: boolean,
hideCancel: boolean, hideCancel: boolean,
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
activeChannelStakedLevel: number,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
@ -92,7 +91,6 @@ const Header = (props: Props) => {
hideCancel, hideCancel,
user, user,
activeChannelClaim, activeChannelClaim,
activeChannelStakedLevel,
} = props; } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent // on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
@ -103,12 +101,6 @@ const Header = (props: Props) => {
const hasBackout = Boolean(backout); const hasBackout = Boolean(backout);
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {}; const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui); const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
const livestreamEnabled = Boolean(
ENABLE_NO_SOURCE_CLAIMS &&
user &&
!user.odysee_live_disabled &&
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
);
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url; const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
// Sign out if they click the "x" when they are on the password prompt // Sign out if they click the "x" when they are on the password prompt
@ -288,7 +280,6 @@ const Header = (props: Props) => {
history={history} history={history}
handleThemeToggle={handleThemeToggle} handleThemeToggle={handleThemeToggle}
currentTheme={currentTheme} currentTheme={currentTheme}
livestreamEnabled={livestreamEnabled}
/> />
</div> </div>
)} )}
@ -405,11 +396,10 @@ type HeaderMenuButtonProps = {
history: { push: (string) => void }, history: { push: (string) => void },
handleThemeToggle: (string) => void, handleThemeToggle: (string) => void,
currentTheme: string, currentTheme: string,
livestreamEnabled: boolean,
}; };
function HeaderMenuButtons(props: HeaderMenuButtonProps) { function HeaderMenuButtons(props: HeaderMenuButtonProps) {
const { authenticated, notificationsEnabled, history, handleThemeToggle, currentTheme, livestreamEnabled } = props; const { authenticated, notificationsEnabled, history, handleThemeToggle, currentTheme } = props;
return ( return (
<div className="header__buttons"> <div className="header__buttons">
@ -437,18 +427,6 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
<Icon aria-hidden icon={ICONS.CHANNEL} /> <Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')} {__('New Channel')}
</MenuItem> </MenuItem>
{/* @if TARGET='web' */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.YOUTUBE_SYNC}`)}>
<Icon aria-hidden icon={ICONS.YOUTUBE} />
{__('Sync YouTube Channel')}
</MenuItem>
{/* @endif */}
{livestreamEnabled && (
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIVESTREAM}`)}>
<Icon aria-hidden icon={ICONS.VIDEO} />
{__('Go Live')}
</MenuItem>
)}
</MenuList> </MenuList>
</Menu> </Menu>
)} )}

View file

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectStakedLevelForChannelUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import LivestreamComment from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
});
export default connect(select)(LivestreamComment);

View file

@ -1,133 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import { parseURI } from 'util/lbryURI';
import MarkdownPreview from 'component/common/markdown-preview';
import Tooltip from 'component/common/tooltip';
import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuButton } from '@reach/menu-button';
import Icon from 'component/common/icon';
import classnames from 'classnames';
import CommentMenuList from 'component/commentMenuList';
import Button from 'component/button';
import CreditAmount from 'component/common/credit-amount';
type Props = {
uri: string,
claim: StreamClaim,
authorUri: string,
commentId: string,
message: string,
commentIsMine: boolean,
stakedLevel: number,
supportAmount: number,
isModerator: boolean,
isGlobalMod: boolean,
isFiat: boolean,
isPinned: boolean,
};
function LivestreamComment(props: Props) {
const {
claim,
uri,
authorUri,
message,
commentIsMine,
commentId,
stakedLevel,
supportAmount,
isModerator,
isGlobalMod,
isFiat,
isPinned,
} = props;
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
const { claimName } = parseURI(authorUri);
return (
<li
className={classnames('livestream-comment', {
'livestream-comment--superchat': supportAmount > 0,
})}
>
{supportAmount > 0 && (
<div className="super-chat livestream-superchat__banner">
<div className="livestream-superchat__banner-corner" />
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestream-superchat__amount" />
</div>
)}
<div className="livestream-comment__body">
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
<div className="livestream-comment__info">
{isGlobalMod && (
<Tooltip label={__('Admin')}>
<span className="comment__badge comment__badge--global-mod">
<Icon icon={ICONS.BADGE_MOD} size={16} />
</span>
</Tooltip>
)}
{isModerator && (
<Tooltip label={__('Moderator')}>
<span className="comment__badge comment__badge--mod">
<Icon icon={ICONS.BADGE_MOD} size={16} />
</span>
</Tooltip>
)}
{commentByOwnerOfContent && (
<Tooltip label={__('Streamer')}>
<span className="comment__badge">
<Icon icon={ICONS.BADGE_STREAMER} size={16} />
</span>
</Tooltip>
)}
<Button
className={classnames('button--uri-indicator comment__author', {
'comment__author--creator': commentByOwnerOfContent,
})}
target="_blank"
navigate={authorUri}
>
{claimName}
</Button>
{isPinned && (
<span className="comment__pin">
<Icon icon={ICONS.PIN} size={14} />
{__('Pinned')}
</span>
)}
<div className="livestream-comment__text">
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} />
</div>
</div>
</div>
<div className="livestream-comment__menu">
<Menu>
<MenuButton className="menu__button">
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
</MenuButton>
<CommentMenuList
uri={uri}
commentId={commentId}
authorUri={authorUri}
commentIsMine={commentIsMine}
disableEdit
isTopLevel
isPinned={isPinned}
disableRemove={supportAmount > 0}
/>
</Menu>
</div>
</li>
);
}
export default LivestreamComment;

View file

@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
import {
makeSelectTopLevelCommentsForUri,
selectIsFetchingComments,
makeSelectSuperChatsForUri,
makeSelectSuperChatTotalAmountForUri,
makeSelectPinnedCommentsForUri,
} from 'redux/selectors/comments';
import LivestreamComments from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
pinnedComments: makeSelectPinnedCommentsForUri(props.uri)(state),
fetchingComments: selectIsFetchingComments(state),
superChats: makeSelectSuperChatsForUri(props.uri)(state),
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
myChannels: selectMyChannelClaims(state),
});
export default connect(select, {
doCommentSocketConnect,
doCommentSocketDisconnect,
doCommentList,
doSuperChatList,
})(LivestreamComments);

View file

@ -1,319 +0,0 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import Spinner from 'component/spinner';
import CommentCreate from 'component/commentCreate';
import LivestreamComment from 'component/livestreamComment';
import Button from 'component/button';
import UriIndicator from 'component/uriIndicator';
import CreditAmount from 'component/common/credit-amount';
import ChannelThumbnail from 'component/channelThumbnail';
import Tooltip from 'component/common/tooltip';
import * as ICONS from 'constants/icons';
type Props = {
uri: string,
claim: ?StreamClaim,
activeViewers: number,
embed?: boolean,
doCommentSocketConnect: (string, string) => void,
doCommentSocketDisconnect: (string) => void,
doCommentList: (string, string, number, number) => void,
comments: Array<Comment>,
pinnedComments: Array<Comment>,
fetchingComments: boolean,
doSuperChatList: (string) => void,
superChats: Array<Comment>,
myChannels: ?Array<ChannelClaim>,
};
const VIEW_MODE_CHAT = 'view_chat';
const VIEW_MODE_SUPER_CHAT = 'view_superchat';
const COMMENT_SCROLL_TIMEOUT = 25;
export default function LivestreamComments(props: Props) {
const {
claim,
uri,
embed,
doCommentSocketConnect,
doCommentSocketDisconnect,
comments: commentsByChronologicalOrder,
pinnedComments,
doCommentList,
fetchingComments,
doSuperChatList,
myChannels,
superChats: superChatsByTipAmount,
} = props;
let superChatsFiatAmount, superChatsTotalAmount;
const commentsRef = React.createRef();
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
const [scrollPos, setScrollPos] = React.useState(0);
const [showPinned, setShowPinned] = React.useState(true);
const claimId = claim && claim.claim_id;
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
// which kind of superchat to display, either
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
const discussionElement = document.querySelector('.livestream__comments');
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
function restoreScrollPos() {
if (discussionElement) {
discussionElement.scrollTop = 0;
}
}
React.useEffect(() => {
if (claimId) {
doCommentList(uri, '', 1, 75);
doSuperChatList(uri);
doCommentSocketConnect(uri, claimId);
}
return () => {
if (claimId) {
doCommentSocketDisconnect(claimId);
}
};
}, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]);
// Register scroll handler (TODO: Should throttle/debounce)
React.useEffect(() => {
function handleScroll() {
if (discussionElement) {
const scrollTop = discussionElement.scrollTop;
if (scrollTop !== scrollPos) {
setScrollPos(scrollTop);
}
}
}
if (discussionElement) {
discussionElement.addEventListener('scroll', handleScroll);
return () => discussionElement.removeEventListener('scroll', handleScroll);
}
}, [discussionElement, scrollPos]);
// Retain scrollPos=0 when receiving new messages.
React.useEffect(() => {
if (discussionElement && commentsLength > 0) {
// Only update comment scroll if the user hasn't scrolled up to view old comments
if (scrollPos >= 0) {
// +ve scrollPos: not scrolled (Usually, there'll be a few pixels beyond 0).
// -ve scrollPos: user scrolled.
const timer = setTimeout(() => {
// Use a timer here to ensure we reset after the new comment has been rendered.
discussionElement.scrollTop = 0;
}, COMMENT_SCROLL_TIMEOUT);
return () => clearTimeout(timer);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [commentsLength]); // (Just respond to 'commentsLength' updates and nothing else)
// sum total amounts for fiat tips and lbc tips
if (superChatsByTipAmount) {
let fiatAmount = 0;
let LBCAmount = 0;
for (const superChat of superChatsByTipAmount) {
if (superChat.is_fiat) {
fiatAmount = fiatAmount + superChat.support_amount;
} else {
LBCAmount = LBCAmount + superChat.support_amount;
}
}
superChatsFiatAmount = fiatAmount;
superChatsTotalAmount = LBCAmount;
}
let superChatsReversed;
// array of superchats organized by fiat or not first, then support amount
if (superChatsByTipAmount) {
const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount));
// for top to bottom display, oldest superchat on top most recent on bottom
superChatsReversed = clonedSuperchats
.sort((a, b) => {
return b.timestamp - a.timestamp;
}); }
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
function isMyComment(channelId: string) {
if (myChannels != null && channelId != null) {
for (let i = 0; i < myChannels.length; i++) {
if (myChannels[i].claim_id === channelId) {
return true;
}
}
}
return false;
}
if (!claim) {
return null;
}
return (
<div className="card livestream__discussion">
<div className="card__header--between livestream-discussion__header">
<div className="livestream-discussion__title">{__('Live discussion')}</div>
{(superChatsTotalAmount || 0) > 0 && (
<div className="recommended-content__toggles">
{/* the superchats in chronological order button */}
<Button
className={classnames('button-toggle', {
'button-toggle--active': viewMode === VIEW_MODE_CHAT,
})}
label={__('Chat')}
onClick={() => {
setViewMode(VIEW_MODE_CHAT);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
livestreamCommentsDiv.scrollTop = livestreamCommentsDiv.scrollHeight;
}}
/>
{/* the button to show superchats listed by most to least support amount */}
<Button
className={classnames('button-toggle', {
'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT,
})}
label={
<>
<CreditAmount amount={superChatsTotalAmount || 0} size={8} /> /
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
</>
}
onClick={() => {
setViewMode(VIEW_MODE_SUPER_CHAT);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
const divHeight = livestreamCommentsDiv.scrollHeight;
livestreamCommentsDiv.scrollTop = divHeight * -1;
}}
/>
</div>
)}
</div>
<>
{fetchingComments && !commentsByChronologicalOrder && (
<div className="main--empty">
<Spinner />
</div>
)}
<div ref={commentsRef} className="livestream__comments-wrapper">
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && (superChatsTotalAmount || 0) > 0 && (
<div className="livestream-superchats__wrapper">
<div className="livestream-superchats__inner">
{superChatsByTipAmount.map((superChat: Comment) => (
<Tooltip key={superChat.comment_id} label={superChat.comment}>
<div className="livestream-superchat">
<div className="livestream-superchat__thumbnail">
<ChannelThumbnail uri={superChat.channel_url} xsmall />
</div>
<div className="livestream-superchat__info">
<UriIndicator uri={superChat.channel_url} link />
<CreditAmount
size={10}
className="livestream-superchat__amount-large"
amount={superChat.support_amount}
isFiat={superChat.is_fiat}
/>
</div>
</div>
</Tooltip>
))}
</div>
</div>
)}
{pinnedComment && showPinned && viewMode === VIEW_MODE_CHAT && (
<div className="livestream-pinned__wrapper">
<LivestreamComment
key={pinnedComment.comment_id}
uri={uri}
authorUri={pinnedComment.channel_url}
commentId={pinnedComment.comment_id}
message={pinnedComment.comment}
supportAmount={pinnedComment.support_amount}
isModerator={pinnedComment.is_moderator}
isGlobalMod={pinnedComment.is_global_mod}
isFiat={pinnedComment.is_fiat}
isPinned={pinnedComment.is_pinned}
commentIsMine={pinnedComment.channel_id && isMyComment(pinnedComment.channel_id)}
/>
<Button
title={__('Dismiss pinned comment')}
button="inverse"
className="close-button"
onClick={() => setShowPinned(false)}
icon={ICONS.REMOVE}
/>
</div>
)}
{/* top to bottom comment display */}
{!fetchingComments && commentsByChronologicalOrder.length > 0 ? (
<div className="livestream__comments">
{viewMode === VIEW_MODE_CHAT &&
commentsToDisplay.map((comment) => (
<LivestreamComment
key={comment.comment_id}
uri={uri}
authorUri={comment.channel_url}
commentId={comment.comment_id}
message={comment.comment}
supportAmount={comment.support_amount}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
/>
))}
{/* listing comments on top of eachother */}
{viewMode === VIEW_MODE_SUPER_CHAT &&
superChatsReversed &&
superChatsReversed.map((comment) => (
<LivestreamComment
key={comment.comment_id}
uri={uri}
authorUri={comment.channel_url}
commentId={comment.comment_id}
message={comment.comment}
supportAmount={comment.support_amount}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
/>
))}
</div>
) : (
<div className="main--empty" style={{ flex: 1 }} />
)}
{scrollPos < 0 && viewMode === VIEW_MODE_CHAT && (
<Button
button="secondary"
className="livestream__comments__scroll-to-recent"
label={__('Recent Comments')}
onClick={restoreScrollPos}
iconRight={ICONS.DOWN}
/>
)}
<div className="livestream__comment-create">
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
</div>
</div>
</>
</div>
);
}

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import {
makeSelectClaimForUri,
makeSelectTagInClaimOrChannelForUri,
makeSelectThumbnailForUri,
} from 'redux/selectors/claims';
import LivestreamLayout from './view';
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
});
export default connect(select)(LivestreamLayout);

View file

@ -1,63 +0,0 @@
// @flow
import { LIVESTREAM_EMBED_URL } from 'constants/livestream';
import React from 'react';
import FileTitleSection from 'component/fileTitleSection';
import LivestreamComments from 'component/livestreamComments';
import { useIsMobile } from 'effects/use-screensize';
type Props = {
uri: string,
claim: ?StreamClaim,
isLive: boolean,
chatDisabled: boolean,
};
export default function LivestreamLayout(props: Props) {
const { claim, uri, isLive, chatDisabled } = props;
const isMobile = useIsMobile();
if (!claim || !claim.signing_channel) {
return null;
}
const channelName = claim.signing_channel.name;
const channelClaimId = claim.signing_channel.claim_id;
return (
<>
<div className="section card-stack">
<div className="file-render file-render--video livestream">
<div className="file-viewer">
<iframe
src={`${LIVESTREAM_EMBED_URL}/${channelClaimId}?skin=odysee&autoplay=1`}
scrolling="no"
allowFullScreen
/>
</div>
</div>
{Boolean(chatDisabled) && (
<div className="help--notice">
{channelName
? __('%channel% has disabled chat for this stream. Enjoy the stream!', { channel: channelName })
: __('This channel has disabled chat for this stream. Enjoy the stream!')}
</div>
)}
{!isLive && (
<div className="help--notice">
{channelName
? __("%channelName% isn't live right now, but the chat is! Check back later to watch the stream.", {
channelName,
})
: __("This channel isn't live right now, but the chat is! Check back later to watch the stream.")}
</div>
)}
{isMobile && <LivestreamComments uri={uri} />}
<FileTitleSection uri={uri} livestream isLive={isLive} />
</div>
</>
);
}

View file

@ -1,9 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import LivestreamLink from './view';
const select = (state, props) => ({
channelClaim: makeSelectClaimForUri(props.uri)(state),
});
export default connect(select)(LivestreamLink);

View file

@ -1,101 +0,0 @@
// @flow
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
import * as CS from 'constants/claim_search';
import React from 'react';
import Card from 'component/common/card';
import ClaimPreview from 'component/claimPreview';
import Lbry from 'lbry';
import { useHistory } from 'react-router';
import { formatLbryUrlForWeb } from 'util/url';
type Props = {
channelClaim: ChannelClaim,
};
export default function LivestreamLink(props: Props) {
const { channelClaim } = props;
const { push } = useHistory();
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
const livestreamChannelId = (channelClaim && channelClaim.claim_id) || ''; // TODO: fail in a safer way, probably
// TODO: pput this back when hubs claims_in_channel are fixed
const isChannelEmpty = !channelClaim || !channelClaim.meta;
// ||
// !channelClaim.meta.claims_in_channel;
React.useEffect(() => {
// Don't search empty channels
if (livestreamChannelId && !isChannelEmpty) {
Lbry.claim_search({
channel_ids: [livestreamChannelId],
page: 1,
page_size: 1,
no_totals: true,
has_no_source: true,
claim_type: ['stream'],
order_by: CS.ORDER_BY_NEW_VALUE,
})
.then((res) => {
if (res && res.items && res.items.length > 0) {
const claim = res.items[0];
// $FlowFixMe Too many Claim GenericClaim etc types.
setLivestreamClaim(claim);
}
})
.catch(() => {});
}
}, [livestreamChannelId, isChannelEmpty]);
React.useEffect(() => {
function fetchIsStreaming() {
// $FlowFixMe livestream API can handle garbage
fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`)
.then((res) => res.json())
.then((res) => {
if (res && res.success && res.data && res.data.live) {
setIsLivestreaming(true);
} else {
setIsLivestreaming(false);
}
})
.catch((e) => {});
}
let interval;
// Only call livestream api if channel has livestream claims
if (livestreamChannelId && livestreamClaim) {
if (!interval) fetchIsStreaming();
interval = setInterval(fetchIsStreaming, 10 * 1000);
}
// Prevent any more api calls on update
if (!livestreamChannelId || !livestreamClaim) {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [livestreamChannelId, livestreamClaim]);
if (!livestreamClaim || !isLivestreaming) {
return null;
}
// gonna pass the wrapper in so I don't have to rewrite the dmca/blocking logic in claimPreview.
const element = (props: { children: any }) => (
<Card
className="livestream__channel-link"
title={__('Live stream in progress')}
onClick={() => {
push(formatLbryUrlForWeb(livestreamClaim.canonical_url));
}}
>
{props.children}
</Card>
);
return <ClaimPreview uri={livestreamClaim.canonical_url} wrapperElement={element} type="inline" />;
}

View file

@ -1,6 +0,0 @@
import { connect } from 'react-redux';
import LivestreamList from './view';
const select = (state) => ({});
export default connect(select)(LivestreamList);

View file

@ -1,78 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
import React from 'react';
import Icon from 'component/common/icon';
import Spinner from 'component/spinner';
import ClaimTilesDiscover from 'component/claimTilesDiscover';
const LIVESTREAM_POLL_IN_MS = 10 * 1000;
export default function LivestreamList() {
const [loading, setLoading] = React.useState(true);
const [livestreamMap, setLivestreamMap] = React.useState();
React.useEffect(() => {
function checkCurrentLivestreams() {
fetch(LIVESTREAM_LIVE_API)
.then((res) => res.json())
.then((res) => {
setLoading(false);
if (!res.data) {
setLivestreamMap({});
return;
}
const livestreamMap = res.data.reduce((acc, curr) => {
return {
...acc,
[curr.claimId]: curr,
};
}, {});
setLivestreamMap(livestreamMap);
})
.catch((err) => {
setLoading(false);
});
}
checkCurrentLivestreams();
let fetchInterval = setInterval(checkCurrentLivestreams, LIVESTREAM_POLL_IN_MS);
return () => {
if (fetchInterval) {
clearInterval(fetchInterval);
}
};
}, []);
return (
<>
{loading && (
<div className="main--empty">
<Spinner delayed />
</div>
)}
{livestreamMap && Object.keys(livestreamMap).length > 0 && (
<ClaimTilesDiscover
showNoSourceClaims
hasNoSource
streamTypes={null}
channelIds={Object.keys(livestreamMap)}
limitClaimsPerChannel={1}
pageSize={50}
renderProperties={(claim) => {
const livestream = livestreamMap[claim.signing_channel.claim_id];
return (
<span className="livestream__viewer-count">
{livestream.viewCount} <Icon icon={ICONS.EYE} />
</span>
);
}}
/>
)}
</>
);
}

View file

@ -37,6 +37,10 @@ export default function Notification(props: Props) {
const [isReplying, setReplying] = React.useState(false); const [isReplying, setReplying] = React.useState(false);
const [quickReply, setQuickReply] = React.useState(); const [quickReply, setQuickReply] = React.useState();
const isIgnoredNotification = notification_rule === RULE.NEW_LIVESTREAM;
if (isIgnoredNotification) {
return null;
}
const isCommentNotification = const isCommentNotification =
notification_rule === RULE.COMMENT || notification_rule === RULE.COMMENT ||
notification_rule === RULE.COMMENT_REPLY || notification_rule === RULE.COMMENT_REPLY ||
@ -83,10 +87,6 @@ export default function Notification(props: Props) {
channelUrl = notification_parameters.dynamic.channel_url; channelUrl = notification_parameters.dynamic.channel_url;
icon = creatorIcon(channelUrl); icon = creatorIcon(channelUrl);
break; break;
case RULE.NEW_LIVESTREAM:
channelUrl = notification_parameters.dynamic.channel_url;
icon = creatorIcon(channelUrl);
break;
case RULE.DAILY_WATCH_AVAILABLE: case RULE.DAILY_WATCH_AVAILABLE:
case RULE.DAILY_WATCH_REMIND: case RULE.DAILY_WATCH_REMIND:
case RULE.MISSED_OUT: case RULE.MISSED_OUT:

View file

@ -28,7 +28,6 @@ type Props = {
fullWidthPage: boolean, fullWidthPage: boolean,
videoTheaterMode: boolean, videoTheaterMode: boolean,
isMarkdown?: boolean, isMarkdown?: boolean,
livestream?: boolean,
chatDisabled: boolean, chatDisabled: boolean,
rightSide?: Node, rightSide?: Node,
backout: { backout: {
@ -52,9 +51,7 @@ function Page(props: Props) {
backout, backout,
videoTheaterMode, videoTheaterMode,
isMarkdown = false, isMarkdown = false,
livestream,
rightSide, rightSide,
chatDisabled,
} = props; } = props;
const { const {
@ -127,8 +124,7 @@ function Page(props: Props) {
'main--file-page': filePage, 'main--file-page': filePage,
'main--settings-page': settingsPage, 'main--settings-page': settingsPage,
'main--markdown': isMarkdown, 'main--markdown': isMarkdown,
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream, 'main--theater-mode': isOnFilePage && videoTheaterMode,
'main--livestream': livestream && !chatDisabled,
})} })}
> >
{children} {children}

View file

@ -2,12 +2,11 @@ import { connect } from 'react-redux';
import { selectBalance } from 'redux/selectors/wallet'; import { selectBalance } from 'redux/selectors/wallet';
import { selectIsStillEditing, makeSelectPublishFormValue } from 'redux/selectors/publish'; import { selectIsStillEditing, makeSelectPublishFormValue } from 'redux/selectors/publish';
import { doUpdatePublishForm, doClearPublish } from 'redux/actions/publish'; import { doUpdatePublishForm, doClearPublish } from 'redux/actions/publish';
import { makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { selectFfmpegStatus } from 'redux/selectors/settings'; import { selectFfmpegStatus } from 'redux/selectors/settings';
import PublishPage from './view'; import PublishPage from './view';
const select = (state, props) => ({ const select = (state) => ({
name: makeSelectPublishFormValue('name')(state), name: makeSelectPublishFormValue('name')(state),
title: makeSelectPublishFormValue('title')(state), title: makeSelectPublishFormValue('title')(state),
filePath: makeSelectPublishFormValue('filePath')(state), filePath: makeSelectPublishFormValue('filePath')(state),
@ -20,7 +19,6 @@ const select = (state, props) => ({
size: makeSelectPublishFormValue('fileSize')(state), size: makeSelectPublishFormValue('fileSize')(state),
duration: makeSelectPublishFormValue('fileDur')(state), duration: makeSelectPublishFormValue('fileDur')(state),
isVid: makeSelectPublishFormValue('fileVid')(state), isVid: makeSelectPublishFormValue('fileVid')(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({

View file

@ -1,5 +1,4 @@
// @flow // @flow
import { SITE_NAME, WEB_PUBLISH_SIZE_LIMIT_GB } from 'config';
import type { Node } from 'react'; import type { Node } from 'react';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
@ -14,11 +13,6 @@ import I18nMessage from 'component/i18nMessage';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import * as PUBLISH_MODES from 'constants/publish_types'; import * as PUBLISH_MODES from 'constants/publish_types';
import PublishName from 'component/publishName'; import PublishName from 'component/publishName';
import CopyableText from 'component/copyableText';
import Empty from 'component/common/empty';
import moment from 'moment';
import classnames from 'classnames';
import ReactPaginate from 'react-paginate';
type Props = { type Props = {
uri: ?string, uri: ?string,
@ -44,12 +38,7 @@ type Props = {
setPublishMode: (string) => void, setPublishMode: (string) => void,
setPrevFileText: (string) => void, setPrevFileText: (string) => void,
header: Node, header: Node,
livestreamData: LivestreamReplayData,
isLivestreamClaim: boolean,
checkLivestreams: (string, ?string, ?string) => void,
channelId: string, channelId: string,
channelSignature: { signature?: string, signing_ts?: string },
isCheckingLivestreams: boolean,
setWaitForFile: (boolean) => void, setWaitForFile: (boolean) => void,
}; };
@ -76,23 +65,10 @@ function PublishFile(props: Props) {
setPublishMode, setPublishMode,
setPrevFileText, setPrevFileText,
header, header,
livestreamData,
isLivestreamClaim,
subtitle, subtitle,
checkLivestreams,
channelId,
channelSignature,
isCheckingLivestreams,
setWaitForFile,
} = props; } = props;
const SOURCE_NONE = 'none';
const SOURCE_SELECT = 'select';
const SOURCE_UPLOAD = 'upload';
const RECOMMENDED_BITRATE = 6000000; const RECOMMENDED_BITRATE = 6000000;
const TV_PUBLISH_SIZE_LIMIT_BYTES = WEB_PUBLISH_SIZE_LIMIT_GB * 1073741824;
const TV_PUBLISH_SIZE_LIMIT_GB_STR = String(WEB_PUBLISH_SIZE_LIMIT_GB);
const PROCESSING_MB_PER_SECOND = 0.5; const PROCESSING_MB_PER_SECOND = 0.5;
const MINUTES_THRESHOLD = 30; const MINUTES_THRESHOLD = 30;
@ -101,35 +77,10 @@ function PublishFile(props: Props) {
const sizeInMB = Number(size) / 1000000; const sizeInMB = Number(size) / 1000000;
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND; const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
const ffmpegAvail = ffmpegStatus.available; const ffmpegAvail = ffmpegStatus.available;
const [oversized, setOversized] = useState(false);
const [currentFile, setCurrentFile] = useState(null); const [currentFile, setCurrentFile] = useState(null);
const [currentFileType, setCurrentFileType] = useState(null); const [currentFileType, setCurrentFileType] = useState(null);
const [optimizeAvail, setOptimizeAvail] = useState(false); const [optimizeAvail, setOptimizeAvail] = useState(false);
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false); const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
const UPLOAD_SIZE_MESSAGE = __(
'%SITE_NAME% uploads are limited to %limit% GB. Download the app for unrestricted publishing.',
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_GB_STR }
);
const fileSelectorModes = [
{ label: __('Upload'), actionName: SOURCE_UPLOAD, icon: ICONS.PUBLISH },
{ label: __('Choose Replay'), actionName: SOURCE_SELECT, icon: ICONS.MENU },
{ label: __('None'), actionName: SOURCE_NONE },
];
const livestreamDataStr = JSON.stringify(livestreamData);
const hasLivestreamData = livestreamData && Boolean(livestreamData.length);
const showSourceSelector = isLivestreamClaim || (hasLivestreamData && mode === PUBLISH_MODES.FILE);
const [fileSelectSource, setFileSelectSource] = useState(
IS_WEB && showSourceSelector && name ? SOURCE_SELECT : SOURCE_UPLOAD
);
// const [showFileUpdate, setShowFileUpdate] = useState(false);
const [selectedFileIndex, setSelectedFileIndex] = useState(null);
const PAGE_SIZE = 4;
const [currentPage, setCurrentPage] = useState(1);
const totalPages =
hasLivestreamData && livestreamData.length > PAGE_SIZE ? Math.ceil(livestreamData.length / PAGE_SIZE) : 1;
// Reset filePath if publish mode changed // Reset filePath if publish mode changed
useEffect(() => { useEffect(() => {
@ -137,46 +88,12 @@ function PublishFile(props: Props) {
if (currentFileType !== 'text/markdown' && !isStillEditing) { if (currentFileType !== 'text/markdown' && !isStillEditing) {
updatePublishForm({ filePath: '' }); updatePublishForm({ filePath: '' });
} }
} else if (mode === PUBLISH_MODES.LIVESTREAM) {
updatePublishForm({ filePath: '' });
} }
}, [currentFileType, mode, isStillEditing, updatePublishForm]); }, [currentFileType, mode, isStillEditing, updatePublishForm]);
// set default file source to select if necessary
useEffect(() => {
if (hasLivestreamData && isLivestreamClaim) {
setWaitForFile(true);
setFileSelectSource(SOURCE_SELECT);
} else if (isLivestreamClaim) {
setFileSelectSource(SOURCE_NONE);
}
}, [hasLivestreamData, isLivestreamClaim, setFileSelectSource]);
const normalizeUrlForProtocol = (url) => {
if (url.startsWith('https://')) {
return url;
} else {
if (url.startsWith('http://')) {
return url;
} else {
return `https://${url}`;
}
}
};
// update remoteUrl when replay selected
useEffect(() => {
const livestreamData = JSON.parse(livestreamDataStr);
if (selectedFileIndex !== null && livestreamData && livestreamData.length) {
updatePublishForm({
remoteFileUrl: normalizeUrlForProtocol(livestreamData[selectedFileIndex].data.fileLocation),
});
}
}, [selectedFileIndex, updatePublishForm, livestreamDataStr]);
useEffect(() => { useEffect(() => {
if (!filePath || filePath === '') { if (!filePath || filePath === '') {
setCurrentFile(''); setCurrentFile('');
setOversized(false);
updateFileInfo(0, 0, false); updateFileInfo(0, 0, false);
} else if (typeof filePath !== 'string') { } else if (typeof filePath !== 'string') {
// Update currentFile file // Update currentFile file
@ -198,10 +115,6 @@ function PublishFile(props: Props) {
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid }); updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
} }
function handlePaginateReplays(page) {
setCurrentPage(page);
}
function getBitrate(size, duration) { function getBitrate(size, duration) {
const s = Number(size); const s = Number(size);
const d = Number(duration); const d = Number(duration);
@ -236,16 +149,6 @@ function PublishFile(props: Props) {
} }
function getUploadMessage() { function getUploadMessage() {
// @if TARGET='web'
if (oversized) {
return (
<p className="help--error">
{UPLOAD_SIZE_MESSAGE}{' '}
<Button button="link" label={__('Upload Guide')} href="https://lbry.com/faq/video-publishing-guide" />
</p>
);
}
// @endif
if (isVid && duration && getBitrate(size, duration) > RECOMMENDED_BITRATE) { if (isVid && duration && getBitrate(size, duration) > RECOMMENDED_BITRATE) {
return ( return (
<p className="help--warning"> <p className="help--warning">
@ -267,32 +170,13 @@ function PublishFile(props: Props) {
} }
if (!!isStillEditing && name) { if (!!isStillEditing && name) {
if (isLivestreamClaim) {
return (
<p className="help">{__('You can upload your own recording or select a replay when your stream is over')}</p>
);
}
return ( return (
<p className="help"> <p className="help">
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })} {__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
</p> </p>
); );
} }
// @if TARGET='web'
if (!isStillEditing) {
return (
<p className="help">
{__(
'For video content, use MP4s in H264/AAC format and a friendly bitrate (under 5 Mbps) and resolution (720p) for more reliable streaming. %SITE_NAME% uploads are restricted to %limit% GB.',
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_GB_STR }
)}{' '}
<Button button="link" label={__('Upload Guide')} href="https://lbry.com/faq/video-publishing-guide" />
</p>
);
}
// @endif
// @if TARGET='app'
if (!isStillEditing) { if (!isStillEditing) {
return ( return (
<p className="help"> <p className="help">
@ -303,7 +187,6 @@ function PublishFile(props: Props) {
</p> </p>
); );
} }
// @endif
} }
function parseName(newName) { function parseName(newName) {
@ -311,27 +194,6 @@ function PublishFile(props: Props) {
return newName.replace(INVALID_URI_CHARS, '-'); return newName.replace(INVALID_URI_CHARS, '-');
} }
function handleFileSource(source) {
if (source === SOURCE_NONE) {
// clear files and remotes...
// https://github.com/lbryio/lbry-desktop/issues/5855
// publish is trying to use one field to share html file blob and string and such
// $FlowFixMe
handleFileChange(false, false);
updatePublishForm({ remoteFileUrl: undefined });
} else if (source === SOURCE_UPLOAD) {
updatePublishForm({ remoteFileUrl: undefined });
} else if (source === SOURCE_SELECT) {
// $FlowFixMe
handleFileChange(false, false);
if (selectedFileIndex !== null) {
updatePublishForm({ remoteFileUrl: livestreamData[selectedFileIndex].data.fileLocation });
}
}
setFileSelectSource(source);
setWaitForFile(source !== SOURCE_NONE);
}
function handleTitleChange(event) { function handleTitleChange(event) {
const title = event.target.value; const title = event.target.value;
// Update title // Update title
@ -348,9 +210,7 @@ function PublishFile(props: Props) {
} }
function handleFileChange(file: WebFile, clearName = true) { function handleFileChange(file: WebFile, clearName = true) {
const { showToast } = props;
window.URL = window.URL || window.webkitURL; window.URL = window.URL || window.webkitURL;
setOversized(false);
// select file, start to select a new one, then cancel // select file, start to select a new one, then cancel
if (!file) { if (!file) {
@ -410,16 +270,6 @@ function PublishFile(props: Props) {
setPublishMode(PUBLISH_MODES.FILE); setPublishMode(PUBLISH_MODES.FILE);
} }
// @if TARGET='web'
// we only need to enforce file sizes on 'web'
if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT_BYTES) {
setOversized(true);
showToast(__(UPLOAD_SIZE_MESSAGE));
updatePublishForm({ filePath: '', name: '' });
return;
}
// @endif
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = { const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
// if electron, we'll set filePath to the path string because SDK is handling publishing. // if electron, we'll set filePath to the path string because SDK is handling publishing.
// File.path will be undefined from web due to browser security, so it will default to the File Object. // File.path will be undefined from web due to browser security, so it will default to the File Object.
@ -472,157 +322,18 @@ function PublishFile(props: Props) {
value={title} value={title}
onChange={handleTitleChange} onChange={handleTitleChange}
/> />
{/* Decide whether to show file upload or replay selector */}
{/* @if TARGET='web' */}
<>
{showSourceSelector && (
<fieldset-section>
<div className="section__actions--between section__actions--align-bottom">
<div>
<label>{__('Replay video available')}</label>
<div className="button-group">
{fileSelectorModes.map((fmode) => (
<Button
key={fmode.label}
icon={fmode.icon || undefined}
iconSize={18}
label={fmode.label}
button="alt"
onClick={() => {
// $FlowFixMe
handleFileSource(fmode.actionName);
}}
className={classnames('button-toggle', {
'button-toggle--active': fileSelectSource === fmode.actionName,
})}
/>
))}
</div>
</div>
{fileSelectSource === SOURCE_SELECT && (
<Button
button="secondary"
label={__('Check for Replays')}
disabled={isCheckingLivestreams}
icon={ICONS.REFRESH}
onClick={() =>
checkLivestreams(channelId, channelSignature.signature, channelSignature.signing_ts)
}
/>
)}
</div>
</fieldset-section>
)}
{fileSelectSource === SOURCE_UPLOAD && showFileUpload && (
<>
<FileSelector
label={__('File')}
disabled={disabled}
currentPath={currentFile}
onFileChosen={handleFileChange}
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
placeholder={__('Select a file to upload')}
/>
{getUploadMessage()}
</>
)}
{fileSelectSource === SOURCE_SELECT && showFileUpload && hasLivestreamData && !isCheckingLivestreams && (
<>
<fieldset-section>
<label>{__('Select Replay')}</label>
<div className="table__wrapper">
<table className="table table--livestream-data">
<tbody>
{livestreamData.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE).map((item, i) => (
<tr
onClick={() => setSelectedFileIndex((currentPage - 1) * PAGE_SIZE + i)}
key={item.id}
className={classnames('livestream__data-row', {
'livestream__data-row--selected': selectedFileIndex === (currentPage - 1) * PAGE_SIZE + i,
})}
>
<td>
<FormField
type="radio"
checked={selectedFileIndex === (currentPage - 1) * PAGE_SIZE + i}
label={null}
onClick={() => setSelectedFileIndex((currentPage - 1) * PAGE_SIZE + i)}
className="livestream__data-row-radio"
/>
</td>
<td>
<div className="livestream_thumb_container">
{item.data.thumbnails.slice(0, 3).map((thumb) => (
<img key={thumb} className="livestream___thumb" src={thumb} />
))}
</div>
</td>
<td>
{`${Math.floor(item.data.fileDuration / 60)} ${
Math.floor(item.data.fileDuration / 60) > 1 ? __('minutes') : __('minute')
}`}
<div className="table__item-label">
{`${moment(item.data.uploadedAt).from(moment())}`}
</div>
</td>
<td>
<CopyableText
primaryButton
copyable={normalizeUrlForProtocol(item.data.fileLocation)}
snackMessage={__('Url copied.')}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</fieldset-section>
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
<fieldset-section>
<ReactPaginate
pageCount={totalPages}
pageRangeDisplayed={2}
previousLabel=""
nextLabel=""
activeClassName="pagination__item--selected"
pageClassName="pagination__item"
previousClassName="pagination__item pagination__item--previous"
nextClassName="pagination__item pagination__item--next"
breakClassName="pagination__item pagination__item--break"
marginPagesDisplayed={2}
onPageChange={(e) => handlePaginateReplays(e.selected + 1)}
forcePage={currentPage - 1}
initialPage={currentPage - 1}
containerClassName="pagination"
/>
</fieldset-section>
</fieldset-group>
</>
)}
{fileSelectSource === SOURCE_SELECT && showFileUpload && !hasLivestreamData && !isCheckingLivestreams && (
<div className="main--empty empty">
<Empty text={__('No replays found.')} />
</div>
)}
{fileSelectSource === SOURCE_SELECT && showFileUpload && isCheckingLivestreams && (
<div className="main--empty empty">
<Spinner small />
</div>
)}
</>
{/* @endif */}
{/* @if TARGET='app' */}
{showFileUpload && ( {showFileUpload && (
<FileSelector <>
label={__('File')} <FileSelector
disabled={disabled} label={__('File')}
currentPath={currentFile} disabled={disabled}
onFileChosen={handleFileChange} currentPath={currentFile}
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files onFileChosen={handleFileChange}
placeholder={__('Select file to upload')} // https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
/> placeholder={__('Select file to upload')}
/>
{getUploadMessage()}
</>
)} )}
{showFileUpload && ( {showFileUpload && (
<FormField <FormField
@ -658,7 +369,6 @@ function PublishFile(props: Props) {
</I18nMessage> </I18nMessage>
</p> </p>
)} )}
{/* @endif */}
{isPublishPost && ( {isPublishPost && (
<PostEditor <PostEditor
label={__('Post --[noun, markdown post tab button]--')} label={__('Post --[noun, markdown post tab button]--')}

View file

@ -15,7 +15,7 @@ import {
selectIsResolvingPublishUris, selectIsResolvingPublishUris,
selectMyClaimForUri, selectMyClaimForUri,
} from 'redux/selectors/publish'; } from 'redux/selectors/publish';
import { selectMyChannelClaims, makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import { selectMyChannelClaims } from 'redux/selectors/claims';
import * as RENDER_MODES from 'constants/file_render_modes'; import * as RENDER_MODES from 'constants/file_render_modes';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { doClaimInitialRewards } from 'redux/actions/rewards'; import { doClaimInitialRewards } from 'redux/actions/rewards';
@ -45,7 +45,6 @@ const select = (state) => {
user: selectUser(state), user: selectUser(state),
// The winning claim for a short lbry uri // The winning claim for a short lbry uri
amountNeededForTakeover: selectTakeOverAmount(state), amountNeededForTakeover: selectTakeOverAmount(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(permanentUrl)(state),
isPostClaim, isPostClaim,
permanentUrl, permanentUrl,
// My previously published claims under this short lbry uri // My previously published claims under this short lbry uri

View file

@ -8,9 +8,8 @@
File upload is carried out in the background by that function. File upload is carried out in the background by that function.
*/ */
import { SITE_NAME, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config'; import { SITE_NAME } from 'config';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Lbry from 'lbry';
import { buildURI, isURIValid, isNameValid } from 'util/lbryURI'; import { buildURI, isURIValid, isNameValid } from 'util/lbryURI';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import Button from 'component/button'; import Button from 'component/button';
@ -29,13 +28,9 @@ import I18nMessage from 'component/i18nMessage';
import * as PUBLISH_MODES from 'constants/publish_types'; import * as PUBLISH_MODES from 'constants/publish_types';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import { toHex } from 'util/hex';
import { LIVESTREAM_REPLAY_API } from 'constants/livestream';
// @if TARGET='app'
import fs from 'fs'; import fs from 'fs';
import tempy from 'tempy'; import tempy from 'tempy';
// @endif
type Props = { type Props = {
disabled: boolean, disabled: boolean,
@ -88,7 +83,6 @@ type Props = {
incognito: boolean, incognito: boolean,
user: ?User, user: ?User,
activeChannelStakedLevel: number, activeChannelStakedLevel: number,
isLivestreamClaim: boolean,
isPostClaim: boolean, isPostClaim: boolean,
permanentUrl: ?string, permanentUrl: ?string,
remoteUrl: ?string, remoteUrl: ?string,
@ -127,9 +121,6 @@ function PublishForm(props: Props) {
enablePublishPreview, enablePublishPreview,
activeChannelClaim, activeChannelClaim,
incognito, incognito,
user,
activeChannelStakedLevel,
isLivestreamClaim,
isPostClaim, isPostClaim,
permanentUrl, permanentUrl,
remoteUrl, remoteUrl,
@ -143,46 +134,31 @@ function PublishForm(props: Props) {
const TYPE_PARAM = 'type'; const TYPE_PARAM = 'type';
const uploadType = urlParams.get(TYPE_PARAM); const uploadType = urlParams.get(TYPE_PARAM);
const _uploadType = uploadType && uploadType.toLowerCase(); const _uploadType = uploadType && uploadType.toLowerCase();
const enableLivestream =
ENABLE_NO_SOURCE_CLAIMS &&
user &&
!user.odysee_live_disabled &&
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
// $FlowFixMe // $FlowFixMe
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => { const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
// $FlowFixMe // $FlowFixMe
if (editingURI) { if (editingURI) {
if (isPostClaim) { if (isPostClaim) {
return mode === PUBLISH_MODES.POST; return mode === PUBLISH_MODES.POST;
} else if (isLivestreamClaim) {
return mode === PUBLISH_MODES.LIVESTREAM && enableLivestream;
} else { } else {
return mode === PUBLISH_MODES.FILE; return mode === PUBLISH_MODES.FILE;
} }
} else if (_uploadType) { } else if (_uploadType) {
return mode === _uploadType && (mode !== PUBLISH_MODES.LIVESTREAM || enableLivestream); return mode === _uploadType;
} else { } else {
return mode !== PUBLISH_MODES.LIVESTREAM || enableLivestream; return false;
} }
}); });
const MODE_TO_I18N_STR = { const MODE_TO_I18N_STR = {
[PUBLISH_MODES.FILE]: 'File', [PUBLISH_MODES.FILE]: 'File',
[PUBLISH_MODES.POST]: 'Post --[noun, markdown post tab button]--', [PUBLISH_MODES.POST]: 'Post --[noun, markdown post tab button]--',
[PUBLISH_MODES.LIVESTREAM]: 'Livestream --[noun, livestream tab button]--',
}; };
const [mode, setMode] = React.useState(_uploadType || PUBLISH_MODES.FILE); const [mode, setMode] = React.useState(_uploadType || PUBLISH_MODES.FILE);
const [isCheckingLivestreams, setCheckingLivestreams] = React.useState(false);
let customSubtitle; let customSubtitle;
if (mode === PUBLISH_MODES.LIVESTREAM || isLivestreamClaim) { if (mode === PUBLISH_MODES.POST || isPostClaim) {
if (isLivestreamClaim) {
customSubtitle = __('Update your livestream');
} else {
customSubtitle = __('Prepare an upcoming livestream');
}
} else if (mode === PUBLISH_MODES.POST || isPostClaim) {
if (isPostClaim) { if (isPostClaim) {
customSubtitle = __('Edit your post'); customSubtitle = __('Edit your post');
} else { } else {
@ -206,16 +182,12 @@ function PublishForm(props: Props) {
const [prevFileText, setPrevFileText] = React.useState(''); const [prevFileText, setPrevFileText] = React.useState('');
const [waitForFile, setWaitForFile] = useState(false); const [waitForFile, setWaitForFile] = useState(false);
const [livestreamData, setLivestreamData] = React.useState([]);
const [signedMessage, setSignedMessage] = React.useState({ signature: undefined, signing_ts: undefined });
const signedMessageStr = JSON.stringify(signedMessage);
const TAGS_LIMIT = 5; const TAGS_LIMIT = 5;
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath && !remoteUrl; const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath && !remoteUrl;
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === ''); const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing; const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
const isInProgress = filePath || editingURI || name || title; const isInProgress = filePath || editingURI || name || title;
const activeChannelName = activeChannelClaim && activeChannelClaim.name; const activeChannelName = activeChannelClaim && activeChannelClaim.name;
const activeChannelClaimStr = JSON.stringify(activeChannelClaim);
// Editing content info // Editing content info
const fileMimeType = const fileMimeType =
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
@ -251,27 +223,6 @@ function PublishForm(props: Props) {
const [previewing, setPreviewing] = React.useState(false); const [previewing, setPreviewing] = React.useState(false);
React.useEffect(() => {
if (activeChannelClaimStr) {
const channelClaim = JSON.parse(activeChannelClaimStr);
const message = 'get-claim-id-replays';
setSignedMessage({ signature: null, signing_ts: null });
// ensure we have a channel
if (channelClaim.claim_id) {
Lbry.channel_sign({
channel_id: channelClaim.claim_id,
hexdata: toHex(message),
})
.then((data) => {
setSignedMessage(data);
})
.catch((error) => {
setSignedMessage({ signature: null, signing_ts: null });
});
}
}
}, [activeChannelClaimStr, setSignedMessage]);
useEffect(() => { useEffect(() => {
if (!hasClaimedInitialRewards) { if (!hasClaimedInitialRewards) {
claimInitialRewards(); claimInitialRewards();
@ -286,32 +237,6 @@ function PublishForm(props: Props) {
} }
}, [modal]); }, [modal]);
// move this to lbryinc OR to a file under ui, and/or provide a standardized livestreaming config.
function fetchLivestreams(channelId, signature, timestamp) {
setCheckingLivestreams(true);
fetch(`${LIVESTREAM_REPLAY_API}/${channelId}?signature=${signature || ''}&signing_ts=${timestamp || ''}`) // claimChannelId
.then((res) => res.json())
.then((res) => {
if (!res || !res.data) {
setLivestreamData([]);
}
setLivestreamData(res.data);
setCheckingLivestreams(false);
})
.catch((e) => {
setLivestreamData([]);
setCheckingLivestreams(false);
});
}
useEffect(() => {
const signedMessage = JSON.parse(signedMessageStr);
if (claimChannelId && signedMessage.signature) {
fetchLivestreams(claimChannelId, signedMessage.signature, signedMessage.signing_ts);
}
}, [claimChannelId, signedMessageStr]);
const isLivestreamMode = mode === PUBLISH_MODES.LIVESTREAM;
let submitLabel; let submitLabel;
if (isClaimingInitialRewards) { if (isClaimingInitialRewards) {
@ -319,8 +244,6 @@ function PublishForm(props: Props) {
} else if (publishing) { } else if (publishing) {
if (isStillEditing) { if (isStillEditing) {
submitLabel = __('Saving...'); submitLabel = __('Saving...');
} else if (isLivestreamMode) {
submitLabel = __('Creating...');
} else { } else {
submitLabel = __('Uploading...'); submitLabel = __('Uploading...');
} }
@ -329,8 +252,6 @@ function PublishForm(props: Props) {
} else { } else {
if (isStillEditing) { if (isStillEditing) {
submitLabel = __('Save'); submitLabel = __('Save');
} else if (isLivestreamMode) {
submitLabel = __('Create');
} else { } else {
submitLabel = __('Upload'); submitLabel = __('Upload');
} }
@ -398,26 +319,20 @@ function PublishForm(props: Props) {
} }
}, [editingURI, resolveUri]); }, [editingURI, resolveUri]);
// set isMarkdownPost in publish form if so, also update isLivestreamPublish // set isMarkdownPost in publish form if so
useEffect(() => { useEffect(() => {
updatePublishForm({ updatePublishForm({
isMarkdownPost: mode === PUBLISH_MODES.POST, isMarkdownPost: mode === PUBLISH_MODES.POST,
isLivestreamPublish: mode === PUBLISH_MODES.LIVESTREAM,
}); });
}, [mode, updatePublishForm]); }, [mode, updatePublishForm]);
useEffect(() => { useEffect(() => {
if (incognito) { if (incognito) {
updatePublishForm({ channel: undefined }); updatePublishForm({ channel: undefined });
// Anonymous livestreams aren't supported
if (isLivestreamMode) {
setMode(PUBLISH_MODES.FILE);
}
} else if (activeChannelName) { } else if (activeChannelName) {
updatePublishForm({ channel: activeChannelName }); updatePublishForm({ channel: activeChannelName });
} }
}, [activeChannelName, incognito, updatePublishForm, isLivestreamMode]); }, [activeChannelName, incognito, updatePublishForm]);
// set mode based on urlParams 'type' // set mode based on urlParams 'type'
useEffect(() => { useEffect(() => {
@ -437,19 +352,10 @@ function PublishForm(props: Props) {
setMode(PUBLISH_MODES.POST); setMode(PUBLISH_MODES.POST);
return; return;
} }
// LiveStream publish
if (_uploadType === PUBLISH_MODES.LIVESTREAM.toLowerCase()) {
if (enableLivestream) {
setMode(PUBLISH_MODES.LIVESTREAM);
} else {
setMode(PUBLISH_MODES.FILE);
}
return;
}
// Default to standard file publish // Default to standard file publish
setMode(PUBLISH_MODES.FILE); setMode(PUBLISH_MODES.FILE);
}, [_uploadType, enableLivestream]); }, [_uploadType]);
// if we have a type urlparam, update it? necessary? // if we have a type urlparam, update it? necessary?
useEffect(() => { useEffect(() => {
@ -509,7 +415,7 @@ function PublishForm(props: Props) {
} }
} }
// Publish file // Publish file
if (mode === PUBLISH_MODES.FILE || isLivestreamMode) { if (mode === PUBLISH_MODES.FILE) {
runPublish = true; runPublish = true;
} }
@ -555,7 +461,7 @@ function PublishForm(props: Props) {
// Editing claim uri // Editing claim uri
return ( return (
<div className="card-stack"> <div className="card-stack">
<ChannelSelect hideAnon={isLivestreamMode} disabled={disabled} /> <ChannelSelect disabled={disabled} />
<PublishFile <PublishFile
uri={permanentUrl} uri={permanentUrl}
@ -565,13 +471,9 @@ function PublishForm(props: Props) {
inProgress={isInProgress} inProgress={isInProgress}
setPublishMode={setMode} setPublishMode={setMode}
setPrevFileText={setPrevFileText} setPrevFileText={setPrevFileText}
livestreamData={livestreamData}
subtitle={customSubtitle} subtitle={customSubtitle}
setWaitForFile={setWaitForFile} setWaitForFile={setWaitForFile}
isCheckingLivestreams={isCheckingLivestreams}
checkLivestreams={fetchLivestreams}
channelId={claimChannelId} channelId={claimChannelId}
channelSignature={signedMessage}
header={ header={
<> <>
{AVAILABLE_MODES.map((modeName) => ( {AVAILABLE_MODES.map((modeName) => (
@ -595,7 +497,7 @@ function PublishForm(props: Props) {
{!publishing && ( {!publishing && (
<div className={classnames({ 'card--disabled': formDisabled })}> <div className={classnames({ 'card--disabled': formDisabled })}>
{mode !== PUBLISH_MODES.POST && <PublishDescription disabled={formDisabled} />} {mode !== PUBLISH_MODES.POST && <PublishDescription disabled={formDisabled} />}
<Card actions={<SelectThumbnail livestreamdData={livestreamData} />} /> <Card actions={<SelectThumbnail />} />
<TagsSelect <TagsSelect
suggestMature suggestMature
disableAutoFocus disableAutoFocus
@ -624,7 +526,7 @@ function PublishForm(props: Props) {
/> />
<PublishBid disabled={isStillEditing || formDisabled} /> <PublishBid disabled={isStillEditing || formDisabled} />
{!isLivestreamMode && <PublishPrice disabled={formDisabled} />} <PublishPrice disabled={formDisabled} />
<PublishAdditionalOptions disabled={formDisabled} /> <PublishAdditionalOptions disabled={formDisabled} />
</div> </div>
)} )}

View file

@ -46,8 +46,6 @@ import InvitedPage from 'page/invited';
import LibraryPage from 'page/library'; import LibraryPage from 'page/library';
import ListBlockedPage from 'page/listBlocked'; import ListBlockedPage from 'page/listBlocked';
import ListsPage from 'page/lists'; import ListsPage from 'page/lists';
import LiveStreamSetupPage from 'page/livestreamSetup';
import LivestreamCurrentPage from 'page/livestreamCurrent';
import OwnComments from 'page/ownComments'; import OwnComments from 'page/ownComments';
import PasswordResetPage from 'page/passwordReset'; import PasswordResetPage from 'page/passwordReset';
import PasswordSetPage from 'page/passwordSet'; import PasswordSetPage from 'page/passwordSet';
@ -306,8 +304,6 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_CREATOR}`} component={SettingsCreatorPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamSetupPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM_CURRENT}`} component={LivestreamCurrentPage} />
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} /> <PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
<PrivateRoute {...props} path={`/$/${PAGES.RECEIVE}`} component={ReceivePage} /> <PrivateRoute {...props} path={`/$/${PAGES.RECEIVE}`} component={ReceivePage} />
<PrivateRoute {...props} path={`/$/${PAGES.SEND}`} component={SendPage} /> <PrivateRoute {...props} path={`/$/${PAGES.SEND}`} component={SendPage} />

View file

@ -463,16 +463,6 @@ export const REPORT_CONTENT_STARTED = 'REPORT_CONTENT_STARTED';
export const REPORT_CONTENT_COMPLETED = 'REPORT_CONTENT_COMPLETED'; export const REPORT_CONTENT_COMPLETED = 'REPORT_CONTENT_COMPLETED';
export const REPORT_CONTENT_FAILED = 'REPORT_CONTENT_FAILED'; export const REPORT_CONTENT_FAILED = 'REPORT_CONTENT_FAILED';
// Livestream
export const FETCH_NO_SOURCE_CLAIMS_STARTED = 'FETCH_NO_SOURCE_CLAIMS_STARTED';
export const FETCH_NO_SOURCE_CLAIMS_COMPLETED = 'FETCH_NO_SOURCE_CLAIMS_COMPLETED';
export const FETCH_NO_SOURCE_CLAIMS_FAILED = 'FETCH_NO_SOURCE_CLAIMS_FAILED';
export const VIEWERS_RECEIVED = 'VIEWERS_RECEIVED';
export const FETCH_ACTIVE_LIVESTREAMS_STARTED = 'FETCH_ACTIVE_LIVESTREAMS_STARTED';
export const FETCH_ACTIVE_LIVESTREAMS_FAILED = 'FETCH_ACTIVE_LIVESTREAMS_FAILED';
export const FETCH_ACTIVE_LIVESTREAMS_SKIPPED = 'FETCH_ACTIVE_LIVESTREAMS_SKIPPED';
export const FETCH_ACTIVE_LIVESTREAMS_COMPLETED = 'FETCH_ACTIVE_LIVESTREAMS_COMPLETED';
// Blacklist // Blacklist
export const FETCH_BLACK_LISTED_CONTENT_STARTED = 'FETCH_BLACK_LISTED_CONTENT_STARTED'; export const FETCH_BLACK_LISTED_CONTENT_STARTED = 'FETCH_BLACK_LISTED_CONTENT_STARTED';
export const FETCH_BLACK_LISTED_CONTENT_COMPLETED = 'FETCH_BLACK_LISTED_CONTENT_COMPLETED'; export const FETCH_BLACK_LISTED_CONTENT_COMPLETED = 'FETCH_BLACK_LISTED_CONTENT_COMPLETED';

View file

@ -1,6 +1,5 @@
export const FF_MAX_CHARS_DEFAULT = 2000; export const FF_MAX_CHARS_DEFAULT = 2000;
export const FF_MAX_CHARS_IN_COMMENT = 2000; export const FF_MAX_CHARS_IN_COMMENT = 2000;
export const FF_MAX_CHARS_IN_LIVESTREAM_COMMENT = 300;
export const FF_MAX_CHARS_IN_DESCRIPTION = 5000; export const FF_MAX_CHARS_IN_DESCRIPTION = 5000;
export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500; export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500;
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255; export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;

View file

@ -158,9 +158,6 @@ export const CHEESE = 'Cheese';
export const PEACE = 'Peace'; export const PEACE = 'Peace';
export const PORK_BUN = 'PorkBun'; export const PORK_BUN = 'PorkBun';
export const MIND_BLOWN = 'MindBlown'; export const MIND_BLOWN = 'MindBlown';
export const LIVESTREAM = 'Livestream';
export const LIVESTREAM_SOLID = 'LivestreamSolid';
export const LIVESTREAM_MONOCHROME = 'LivestreamMono';
export const STACK = 'stack'; export const STACK = 'stack';
export const TIME = 'time'; export const TIME = 'time';
export const GLOBE = 'globe'; export const GLOBE = 'globe';

View file

@ -1,4 +0,0 @@
export const LIVESTREAM_EMBED_URL = 'https://player.live.odysee.com/odysee';
export const LIVESTREAM_LIVE_API = 'https://api.live.odysee.com/v1/odysee/live';
export const LIVESTREAM_REPLAY_API = 'https://api.live.odysee.com/v1/replays/odysee';
export const LIVESTREAM_RTMP_URL = 'rtmp://stream.odysee.com/live';

View file

@ -34,8 +34,6 @@ export const PAGE_TITLE = {
[PAGES.HELP]: 'Help', [PAGES.HELP]: 'Help',
[PAGES.INVITE]: 'Invite', [PAGES.INVITE]: 'Invite',
[PAGES.LISTS]: 'Lists', [PAGES.LISTS]: 'Lists',
[PAGES.LIVESTREAM]: 'Go Live on Odysee',
[PAGES.LIVESTREAM_CURRENT]: 'Live (Experimental)',
[PAGES.NOTIFICATIONS]: 'Notifications', [PAGES.NOTIFICATIONS]: 'Notifications',
[PAGES.RECEIVE]: 'Your address', [PAGES.RECEIVE]: 'Your address',
[PAGES.REPORT]: 'Report an issue or request a feature', [PAGES.REPORT]: 'Report an issue or request a feature',

View file

@ -73,7 +73,5 @@ exports.SWAP = 'swap';
exports.CHANNEL_NEW = 'channel/new'; exports.CHANNEL_NEW = 'channel/new';
exports.NOTIFICATIONS = 'notifications'; exports.NOTIFICATIONS = 'notifications';
exports.YOUTUBE_SYNC = 'youtube'; exports.YOUTUBE_SYNC = 'youtube';
exports.LIVESTREAM = 'livestream';
exports.LIVESTREAM_CURRENT = 'live';
exports.GENERAL = 'general'; exports.GENERAL = 'general';
exports.LIST = 'list'; exports.LIST = 'list';

View file

@ -1,3 +1,2 @@
export const FILE = 'file'; export const FILE = 'file';
export const POST = 'post'; export const POST = 'post';
export const LIVESTREAM = 'livestream';

View file

@ -1,18 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import ModalPublishSuccess from './view'; import ModalPublishSuccess from './view';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { doClearPublish } from 'redux/actions/publish'; import { doClearPublish } from 'redux/actions/publish';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
});
const perform = (dispatch) => ({ const perform = (dispatch) => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
clearPublish: () => dispatch(doClearPublish()), clearPublish: () => dispatch(doClearPublish()),
navigate: (path) => dispatch(push(path)), navigate: (path) => dispatch(push(path)),
}); });
export default connect(select, perform)(ModalPublishSuccess); export default connect(null, perform)(ModalPublishSuccess);

View file

@ -15,7 +15,6 @@ type Props = {
isEdit: boolean, isEdit: boolean,
filePath: ?string, filePath: ?string,
lbryFirstError: ?string, lbryFirstError: ?string,
claim: Claim,
}; };
class ModalPublishSuccess extends React.PureComponent<Props> { class ModalPublishSuccess extends React.PureComponent<Props> {
@ -24,13 +23,10 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
clearPublish(); clearPublish();
} }
render() { render() {
const { closeModal, clearPublish, navigate, uri, isEdit, filePath, lbryFirstError, claim } = this.props; const { closeModal, clearPublish, navigate, uri, isEdit, filePath, lbryFirstError } = this.props;
// $FlowFixMe // $FlowFixMe
const livestream = claim && claim.value && claim.value_type === 'stream' && !claim.value.source;
let contentLabel; let contentLabel;
if (livestream) { if (isEdit) {
contentLabel = __('Livestream Created');
} else if (isEdit) {
contentLabel = __('Update published'); contentLabel = __('Update published');
} else { } else {
contentLabel = __('File published'); contentLabel = __('File published');
@ -39,10 +35,6 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
let publishMessage; let publishMessage;
if (isEdit) { if (isEdit) {
publishMessage = __('Your update is now pending. It will take a few minutes to appear for other users.'); publishMessage = __('Your update is now pending. It will take a few minutes to appear for other users.');
} else if (livestream) {
publishMessage = __(
'Your livestream is now pending. You will be able to start shortly at the streaming dashboard.'
);
} else { } else {
publishMessage = __('Your content will be live shortly.'); publishMessage = __('Your content will be live shortly.');
} }
@ -54,7 +46,7 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
return ( return (
<Modal isOpen type="card" contentLabel={__(contentLabel)} onAborted={handleClose}> <Modal isOpen type="card" contentLabel={__(contentLabel)} onAborted={handleClose}>
<Card <Card
title={livestream ? __('Livestream Created') : __('Success')} title={__('Success')}
subtitle={publishMessage} subtitle={publishMessage}
body={ body={
<React.Fragment> <React.Fragment>
@ -75,28 +67,15 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
} }
actions={ actions={
<div className="section__actions"> <div className="section__actions">
{!livestream && ( <Button
<Button button="primary"
button="primary" label={__('View My Uploads')}
label={__('View My Uploads')} onClick={() => {
onClick={() => { clearPublish();
clearPublish(); navigate(`/$/${PAGES.UPLOADS}`);
navigate(`/$/${PAGES.UPLOADS}`); closeModal();
closeModal(); }}
}} />
/>
)}
{livestream && (
<Button
button="primary"
label={__('View My Dashboard')}
onClick={() => {
clearPublish();
navigate(`/$/${PAGES.LIVESTREAM}`);
closeModal();
}}
/>
)}
<Button button="link" label={__('Close')} onClick={handleClose} /> <Button button="link" label={__('Close')} onClick={handleClose} />
</div> </div>
} }

View file

@ -2,26 +2,21 @@ import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import ModalPublishPreview from './view'; import ModalPublishPreview from './view';
import { makeSelectPublishFormValue, selectPublishFormValues, selectIsStillEditing } from 'redux/selectors/publish'; import { makeSelectPublishFormValue, selectPublishFormValues, selectIsStillEditing } from 'redux/selectors/publish';
import { selectMyChannelClaims, makeSelectClaimIsStreamPlaceholder } from 'redux/selectors/claims'; import { selectMyChannelClaims } from 'redux/selectors/claims';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings'; import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings';
import { doPublishDesktop } from 'redux/actions/publish'; import { doPublishDesktop } from 'redux/actions/publish';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
const select = (state, props) => { const select = (state) => {
const editingUri = makeSelectPublishFormValue('editingURI')(state);
return { return {
...selectPublishFormValues(state), ...selectPublishFormValues(state),
myChannels: selectMyChannelClaims(state), myChannels: selectMyChannelClaims(state),
isVid: makeSelectPublishFormValue('fileVid')(state), isVid: makeSelectPublishFormValue('fileVid')(state),
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
publishing: makeSelectPublishFormValue('publishing')(state), publishing: makeSelectPublishFormValue('publishing')(state),
remoteFile: makeSelectPublishFormValue('remoteFileUrl')(state),
isStillEditing: selectIsStillEditing(state), isStillEditing: selectIsStillEditing(state),
ffmpegStatus: selectFfmpegStatus(state), ffmpegStatus: selectFfmpegStatus(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state), enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(editingUri)(state),
}; };
}; };

View file

@ -45,7 +45,6 @@ type Props = {
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
publishSuccess: boolean, publishSuccess: boolean,
publishing: boolean, publishing: boolean,
isLivestreamClaim: boolean,
remoteFile: string, remoteFile: string,
}; };
@ -75,12 +74,9 @@ const ModalPublishPreview = (props: Props) => {
setEnablePublishPreview, setEnablePublishPreview,
isStillEditing, isStillEditing,
myChannels, myChannels,
publishSuccess,
publishing, publishing,
publish, publish,
closeModal, closeModal,
isLivestreamClaim,
remoteFile,
} = props; } = props;
const maxCharsBeforeOverflow = 128; const maxCharsBeforeOverflow = 128;
@ -99,20 +95,7 @@ const ModalPublishPreview = (props: Props) => {
return uri; return uri;
}, [uri]); }, [uri]);
const livestream =
(uri && isLivestreamClaim) ||
// $FlowFixMe
(previewResponse.outputs[0] && previewResponse.outputs[0].value && !previewResponse.outputs[0].value.source);
// leave the confirm modal up if we're not going straight to upload/reflecting // leave the confirm modal up if we're not going straight to upload/reflecting
// @if TARGET='web'
React.useEffect(() => {
if (publishing && !livestream) {
closeModal();
} else if (publishSuccess) {
closeModal();
}
}, [publishSuccess, publishing, livestream]);
// @endif
function onConfirmed() { function onConfirmed() {
// Publish for real: // Publish for real:
publish(getFilePathName(filePath), false); publish(getFilePathName(filePath), false);
@ -147,13 +130,7 @@ const ModalPublishPreview = (props: Props) => {
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available; const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
let modalTitle; let modalTitle;
if (isStillEditing) { if (isStillEditing) {
if (livestream) { modalTitle = __('Confirm Edit');
modalTitle = __('Confirm Update');
} else {
modalTitle = __('Confirm Edit');
}
} else if (livestream) {
modalTitle = __('Create Livestream');
} else { } else {
modalTitle = __('Confirm Upload'); modalTitle = __('Confirm Upload');
} }
@ -162,16 +139,12 @@ const ModalPublishPreview = (props: Props) => {
if (!publishing) { if (!publishing) {
if (isStillEditing) { if (isStillEditing) {
confirmBtnText = __('Save'); confirmBtnText = __('Save');
} else if (livestream) {
confirmBtnText = __('Create');
} else { } else {
confirmBtnText = __('Upload'); confirmBtnText = __('Upload');
} }
} else { } else {
if (isStillEditing) { if (isStillEditing) {
confirmBtnText = __('Saving'); confirmBtnText = __('Saving');
} else if (livestream) {
confirmBtnText = __('Creating');
} else { } else {
confirmBtnText = __('Uploading'); confirmBtnText = __('Uploading');
} }
@ -240,8 +213,7 @@ const ModalPublishPreview = (props: Props) => {
<div className="section"> <div className="section">
<table className="table table--condensed table--publish-preview"> <table className="table table--condensed table--publish-preview">
<tbody> <tbody>
{!livestream && !isMarkdownPost && createRow(__('File'), getFilePathName(filePath))} {!isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
{livestream && remoteFile && createRow(__('Replay'), __('Remote File Selected'))}
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))} {isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
{createRow(__('Title'), formattedTitle)} {createRow(__('Title'), formattedTitle)}
{createRow(__('Description'), descriptionValue)} {createRow(__('Description'), descriptionValue)}

View file

@ -3,8 +3,6 @@ import { connect } from 'react-redux';
import { doResolveUri } from 'redux/actions/claims'; import { doResolveUri } from 'redux/actions/claims';
import { makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectClaimForUri } from 'redux/selectors/claims';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
import { selectActiveLivestreams } from 'redux/selectors/livestream';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectFollowedTags } from 'redux/selectors/tags'; import { selectFollowedTags } from 'redux/selectors/tags';
import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags';
@ -22,12 +20,10 @@ const select = (state, props) => {
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null, repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state), tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
activeLivestreams: selectActiveLivestreams(state),
}; };
}; };
export default connect(select, { export default connect(select, {
doToggleTagFollowDesktop, doToggleTagFollowDesktop,
doResolveUri, doResolveUri,
doFetchActiveLivestreams,
})(Tags); })(Tags);

View file

@ -5,7 +5,6 @@ import {
makeSelectMetadataForUri, makeSelectMetadataForUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectTagInClaimOrChannelForUri, makeSelectTagInClaimOrChannelForUri,
makeSelectClaimIsStreamPlaceholder,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import { doFetchFileInfo } from 'redux/actions/file_info'; import { doFetchFileInfo } from 'redux/actions/file_info';
@ -34,7 +33,6 @@ const select = (state, props) => {
renderMode: makeSelectFileRenderModeForUri(props.uri)(state), renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state), videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state), commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
collection: makeSelectCollectionForId(collectionId)(state), collection: makeSelectCollectionForId(collectionId)(state),
collectionId, collectionId,
position: makeSelectContentPositionForUri(props.uri)(state), position: makeSelectContentPositionForUri(props.uri)(state),

View file

@ -1,5 +1,4 @@
// @flow // @flow
import * as PAGES from 'constants/pages';
import * as React from 'react'; import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Page from 'component/page'; import Page from 'component/page';
@ -11,8 +10,6 @@ import FileRenderDownload from 'component/fileRenderDownload';
import RecommendedContent from 'component/recommendedContent'; import RecommendedContent from 'component/recommendedContent';
import CollectionContent from 'component/collectionContentSidebar'; import CollectionContent from 'component/collectionContentSidebar';
import CommentsList from 'component/commentsList'; import CommentsList from 'component/commentsList';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import PostViewer from 'component/postViewer'; import PostViewer from 'component/postViewer';
@ -36,7 +33,6 @@ type Props = {
videoTheaterMode: boolean, videoTheaterMode: boolean,
claimIsMine: boolean, claimIsMine: boolean,
commentsDisabled: boolean, commentsDisabled: boolean,
isLivestream: boolean,
clearPosition: (string) => void, clearPosition: (string) => void,
position: number, position: number,
}; };
@ -55,12 +51,9 @@ function FilePage(props: Props) {
linkedCommentId, linkedCommentId,
setPrimaryUri, setPrimaryUri,
videoTheaterMode, videoTheaterMode,
claimIsMine,
commentsDisabled, commentsDisabled,
collection, collection,
collectionId, collectionId,
isLivestream,
clearPosition, clearPosition,
position, position,
} = props; } = props;
@ -190,18 +183,6 @@ function FilePage(props: Props) {
{!isMarkdown && ( {!isMarkdown && (
<div className="file-page__secondary-content"> <div className="file-page__secondary-content">
<div> <div>
{claimIsMine && isLivestream && (
<div className="livestream__creator-message">
<h4>{__('Only visible to you')}</h4>
<I18nMessage>
People who view this link will be redirected to your livestream. Make sure to use this for sharing
so your title and thumbnail are displayed properly.
</I18nMessage>
<div className="section__actions">
<Button button="primary" navigate={`/$/${PAGES.LIVESTREAM}`} label={__('View livestream')} />
</div>
</div>
)}
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />} {RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
{commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />} {commentsDisabled && <Empty text={__('The creator of this content has disabled comments.')} />}
{!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />} {!commentsDisabled && <CommentsList uri={uri} linkedCommentId={linkedCommentId} />}

View file

@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import { doSetPlayingUri } from 'redux/actions/content';
import { doUserSetReferrer } from 'redux/actions/user';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards';
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import LivestreamPage from './view';
const select = (state, props) => ({
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
isAuthenticated: selectUserVerifiedEmail(state),
channelClaim: makeSelectClaimForUri(props.uri)(state),
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
});
export default connect(select, {
doSetPlayingUri,
doResolveUri,
doUserSetReferrer,
})(LivestreamPage);

View file

@ -1,124 +0,0 @@
// @flow
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
import React from 'react';
import Page from 'component/page';
import LivestreamLayout from 'component/livestreamLayout';
import LivestreamComments from 'component/livestreamComments';
import analytics from 'analytics';
import Lbry from 'lbry';
type Props = {
uri: string,
claim: StreamClaim,
doSetPlayingUri: ({ uri: ?string }) => void,
isAuthenticated: boolean,
doUserSetReferrer: (string) => void,
channelClaim: ChannelClaim,
chatDisabled: boolean,
};
export default function LivestreamPage(props: Props) {
const { uri, claim, doSetPlayingUri, isAuthenticated, doUserSetReferrer, channelClaim, chatDisabled } = props;
const [isLive, setIsLive] = React.useState(false);
const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false);
const STREAMING_POLL_INTERVAL_IN_MS = 10000;
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000;
React.useEffect(() => {
let checkClaimsInterval;
function checkHasLivestreamClaim() {
Lbry.claim_search({
channel_ids: [livestreamChannelId],
has_no_source: true,
claim_type: ['stream'],
})
.then((res) => {
if (res && res.items && res.items.length > 0) {
setHasLivestreamClaim(true);
}
})
.catch(() => {});
}
if (livestreamChannelId && !isLive) {
if (!checkClaimsInterval) checkHasLivestreamClaim();
checkClaimsInterval = setInterval(checkHasLivestreamClaim, LIVESTREAM_CLAIM_POLL_IN_MS);
return () => {
if (checkClaimsInterval) {
clearInterval(checkClaimsInterval);
}
};
}
}, [livestreamChannelId, isLive]);
React.useEffect(() => {
let interval;
function checkIsLive() {
// TODO: duplicate code below
// $FlowFixMe livestream API can handle garbage
fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`)
.then((res) => res.json())
.then((res) => {
if (!res || !res.data) {
setIsLive(false);
return;
}
if (res.data.hasOwnProperty('live')) {
setIsLive(res.data.live);
}
});
}
if (livestreamChannelId && hasLivestreamClaim) {
if (!interval) checkIsLive();
interval = setInterval(checkIsLive, STREAMING_POLL_INTERVAL_IN_MS);
return () => {
if (interval) {
clearInterval(interval);
}
};
}
}, [livestreamChannelId, hasLivestreamClaim]);
const stringifiedClaim = JSON.stringify(claim);
React.useEffect(() => {
if (uri && stringifiedClaim) {
const jsonClaim = JSON.parse(stringifiedClaim);
if (jsonClaim) {
const { txid, nout, claim_id: claimId } = jsonClaim;
const outpoint = `${txid}:${nout}`;
analytics.apiLogView(uri, outpoint, claimId);
}
if (!isAuthenticated) {
const uri = jsonClaim.signing_channel && jsonClaim.signing_channel.permanent_url;
if (uri) {
doUserSetReferrer(uri.replace('lbry://', ''));
}
}
}
}, [uri, stringifiedClaim, isAuthenticated]);
React.useEffect(() => {
// Set playing uri to null so the popout player doesnt start playing the dummy claim if a user navigates back
// This can be removed when we start using the app video player, not a LIVESTREAM iframe
doSetPlayingUri({ uri: null });
}, [doSetPlayingUri]);
return (
<Page
className="file-page"
noFooter
livestream
chatDisabled={chatDisabled}
rightSide={!chatDisabled && <LivestreamComments uri={uri} />}
>
<LivestreamLayout uri={uri} isLive={isLive} />
</Page>
);
}

View file

@ -1,9 +0,0 @@
import { connect } from 'react-redux';
import { selectUser } from 'redux/selectors/user';
import LivestreamCurrent from './view';
const select = (state) => ({
user: selectUser(state),
});
export default connect(select)(LivestreamCurrent);

View file

@ -1,34 +0,0 @@
// @flow
import React from 'react';
import LivestreamList from 'component/livestreamList';
import Button from 'component/button';
import Page from 'component/page';
import Yrbl from 'component/yrbl';
type Props = {
user: ?User,
};
export default function LivestreamCurrentPage(props: Props) {
const { user } = props;
const canView = process.env.ENABLE_WIP_FEATURES || (user && user.global_mod);
return (
<Page>
{canView ? (
<LivestreamList />
) : (
<Yrbl
type="sad"
title={__("This page isn't quite ready")}
subtitle={__('Check back later.')}
actions={
<div className="section__actions">
<Button button="primary" navigate="/" label={__('Go Home')} />
</div>
}
/>
)}
</Page>
);
}

View file

@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims';
import { doClearPublish } from 'redux/actions/publish';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { doFetchNoSourceClaims } from 'redux/actions/livestream';
import {
makeSelectPendingLivestreamsForChannelId,
makeSelectLivestreamsForChannelId,
makeSelectIsFetchingLivestreams,
} from 'redux/selectors/livestream';
import LivestreamSetupPage from './view';
import { push } from 'connected-react-router';
const select = (state) => {
const activeChannelClaim = selectActiveChannelClaim(state);
const { claim_id: channelId, name: channelName } = activeChannelClaim || {};
return {
channelName,
channelId,
channels: selectMyChannelClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
activeChannelClaim,
myLivestreamClaims: makeSelectLivestreamsForChannelId(channelId)(state),
pendingClaims: makeSelectPendingLivestreamsForChannelId(channelId)(state),
fetchingLivestreams: makeSelectIsFetchingLivestreams(channelId)(state),
};
};
const perform = (dispatch) => ({
doNewLivestream: (path) => {
dispatch(doClearPublish());
dispatch(push(path));
},
fetchNoSourceClaims: (id) => dispatch(doFetchNoSourceClaims(id)),
});
export default connect(select, perform)(LivestreamSetupPage);

View file

@ -1,324 +0,0 @@
// @flow
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import * as PUBLISH_MODES from 'constants/publish_types';
import I18nMessage from 'component/i18nMessage';
import React from 'react';
import Page from 'component/page';
import Spinner from 'component/spinner';
import Button from 'component/button';
import ChannelSelector from 'component/channelSelector';
import Yrbl from 'component/yrbl';
import Lbry from 'lbry';
import { toHex } from 'util/hex';
import { FormField } from 'component/common/form';
import CopyableText from 'component/copyableText';
import Card from 'component/common/card';
import ClaimList from 'component/claimList';
import usePersistedState from 'effects/use-persisted-state';
import { LIVESTREAM_RTMP_URL } from 'constants/livestream';
type Props = {
channels: Array<ChannelClaim>,
fetchingChannels: boolean,
activeChannelClaim: ?ChannelClaim,
pendingClaims: Array<Claim>,
doNewLivestream: (string) => void,
fetchNoSourceClaims: (string) => void,
myLivestreamClaims: Array<Claim>,
fetchingLivestreams: boolean,
channelId: ?string,
channelName: ?string,
};
export default function LivestreamSetupPage(props: Props) {
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000;
const {
channels,
fetchingChannels,
activeChannelClaim,
pendingClaims,
doNewLivestream,
fetchNoSourceClaims,
myLivestreamClaims,
fetchingLivestreams,
channelId,
channelName,
} = props;
const [sigData, setSigData] = React.useState({ signature: undefined, signing_ts: undefined });
const [showHelp, setShowHelp] = usePersistedState('livestream-help-seen', true);
const hasChannels = channels && channels.length > 0;
const hasLivestreamClaims = Boolean(myLivestreamClaims.length || pendingClaims.length);
function createStreamKey() {
if (!channelId || !channelName || !sigData.signature || !sigData.signing_ts) return null;
return `${channelId}?d=${toHex(channelName)}&s=${sigData.signature}&t=${sigData.signing_ts}`;
}
const streamKey = createStreamKey();
const pendingLength = pendingClaims.length;
const totalLivestreamClaims = pendingClaims.concat(myLivestreamClaims);
const helpText = (
<div className="section__subtitle">
<p>
{__(
`Create a Livestream by first submitting your livestream details and waiting for approval confirmation. This can be done well in advance and will take a few minutes.`
)}{' '}
{__(
`The livestream will not be visible on your channel page until you are live, but you can share the URL in advance.`
)}{' '}
{__(
`Once the your livestream is confirmed, configure your streaming software (OBS, Restream, etc) and input the server URL along with the stream key in it.`
)}
</p>
<p>{__(`To ensure the best streaming experience with OBS, open settings -> output`)}</p>
<p>{__(`Select advanced mode from the dropdown at the top.`)}</p>
<p>{__(`Ensure the following settings are selected under the streaming tab:`)}</p>
<ul>
<li>{__(`Bitrate: 1000 to 2500 kbps`)}</li>
<li>{__(`Keyframes: 1`)}</li>
<li>{__(`Profile: High`)}</li>
<li>{__(`Tune: Zerolatency`)}</li>
</ul>
<p>
{__(`If using other streaming software, make sure the bitrate is below 4500 kbps or the stream will not work.`)}
</p>
<p>
{__(
`After your stream:\nClick the Update button on the content page. This will allow you to select a replay or upload your own edited MP4. Replays are limited to 4 hours and may take a few minutes to show (use the Check For Replays button).`
)}
</p>
<p>{__(`Click Save, then confirm, and you are done!`)}</p>
<p>
{__(
`Note: If you don't plan on publishing your replay, you'll want to delete your livestream and then start with a fresh one next time.`
)}
</p>
</div>
);
React.useEffect(() => {
// ensure we have a channel
if (channelId && channelName) {
Lbry.channel_sign({
channel_id: channelId,
hexdata: toHex(channelName),
})
.then((data) => {
setSigData(data);
})
.catch((error) => {
setSigData({ signature: null, signing_ts: null });
});
}
}, [channelName, channelId, setSigData]);
React.useEffect(() => {
let checkClaimsInterval;
if (!channelId) return;
if (!checkClaimsInterval) {
fetchNoSourceClaims(channelId);
checkClaimsInterval = setInterval(() => fetchNoSourceClaims(channelId), LIVESTREAM_CLAIM_POLL_IN_MS);
}
return () => {
if (checkClaimsInterval) {
clearInterval(checkClaimsInterval);
}
};
}, [channelId, pendingLength, fetchNoSourceClaims]);
return (
<Page>
{fetchingChannels && (
<div className="main--empty">
<Spinner delayed />
</div>
)}
{!fetchingChannels && !hasChannels && (
<Yrbl
type="happy"
title={__("You haven't created a channel yet, let's fix that!")}
actions={
<div className="section__actions">
<Button button="primary" navigate={`/$/${PAGES.CHANNEL_NEW}`} label={__('Create A Channel')} />
</div>
}
/>
)}
{!fetchingChannels && (
<div className="section__actions--between">
<ChannelSelector hideAnon />
<Button button="link" onClick={() => setShowHelp(!showHelp)} label={__('How does this work?')} />
</div>
)}
{fetchingLivestreams && !fetchingChannels && !hasLivestreamClaims && (
<div className="main--empty">
<Spinner delayed />
</div>
)}
<div className="card-stack">
{!fetchingChannels && channelId && (
<>
{showHelp && (
<Card
titleActions={<Button button="close" icon={ICONS.REMOVE} onClick={() => setShowHelp(false)} />}
title={__('Go Live on Odysee')}
subtitle={__(`You're invited to try out our new livestreaming service while in beta!`)}
actions={helpText}
/>
)}
{streamKey && totalLivestreamClaims.length > 0 && (
<Card
className="section"
title={__('Your stream key')}
actions={
<>
<CopyableText
primaryButton
name="stream-server"
label={__('Stream server')}
copyable={LIVESTREAM_RTMP_URL}
snackMessage={__('Copied stream server URL.')}
/>
<CopyableText
primaryButton
enableInputMask
name="livestream-key"
label={__('Stream key')}
copyable={streamKey}
snackMessage={__('Copied stream key.')}
/>
</>
}
/>
)}
{totalLivestreamClaims.length > 0 ? (
<>
{Boolean(pendingClaims.length) && (
<div className="section">
<ClaimList
header={__('Your pending livestream uploads')}
uris={pendingClaims.map((claim) => claim.permanent_url)}
/>
</div>
)}
{Boolean(myLivestreamClaims.length) && (
<div className="section">
<ClaimList
header={__('Your livestream uploads')}
empty={
<I18nMessage
tokens={{
check_again: (
<Button
button="link"
onClick={() => fetchNoSourceClaims(channelId)}
label={__('Check again')}
/>
),
}}
>
Nothing here yet. %check_again%
</I18nMessage>
}
uris={myLivestreamClaims
.filter(
(claim) => !pendingClaims.some((pending) => pending.permanent_url === claim.permanent_url)
)
.map((claim) => claim.permanent_url)}
/>
</div>
)}
</>
) : (
<Yrbl
className="livestream__publish-intro"
title={__('No livestream publishes found')}
subtitle={__(
'You need to upload your livestream details before you can go live. If you already created one in this channel, it should appear soon.'
)}
actions={
<div className="section__actions">
<Button
button="primary"
onClick={() =>
doNewLivestream(`/$/${PAGES.UPLOAD}?type=${PUBLISH_MODES.LIVESTREAM.toLowerCase()}`)
}
label={__('Create A Livestream')}
/>
<Button
button="alt"
onClick={() => {
fetchNoSourceClaims(channelId);
}}
label={__('Check again...')}
/>
</div>
}
/>
)}
{/* Debug Stuff */}
{streamKey && false && activeChannelClaim && (
<div style={{ marginTop: 'var(--spacing-l)' }}>
<h3>Debug Info</h3>
{/* Channel ID */}
<FormField
name={'channelId'}
label={'Channel ID'}
type={'text'}
defaultValue={activeChannelClaim.claim_id}
readOnly
/>
{/* Signature */}
<FormField
name={'signature'}
label={'Signature'}
type={'text'}
defaultValue={sigData.signature}
readOnly
/>
{/* Signature TS */}
<FormField
name={'signaturets'}
label={'Signature Timestamp'}
type={'text'}
defaultValue={sigData.signing_ts}
readOnly
/>
{/* Hex Data */}
<FormField
name={'datahex'}
label={'Hex Data'}
type={'text'}
defaultValue={toHex(activeChannelClaim.name)}
readOnly
/>
{/* Channel Public Key */}
<FormField
name={'channelpublickey'}
label={'Public Key'}
type={'text'}
defaultValue={activeChannelClaim.value.public_key}
readOnly
/>
</div>
)}
</>
)}
</div>
</Page>
);
}

View file

@ -57,7 +57,6 @@ export default function OwnComments(props: Props) {
return comments.map((comment) => { return comments.map((comment) => {
const contentClaim = claimsById[comment.claim_id]; const contentClaim = claimsById[comment.claim_id];
const isChannel = contentClaim && contentClaim.value_type === 'channel'; const isChannel = contentClaim && contentClaim.value_type === 'channel';
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
return ( return (
<div key={comment.comment_id} className="comments-own card__main-actions"> <div key={comment.comment_id} className="comments-own card__main-actions">
@ -68,7 +67,6 @@ export default function OwnComments(props: Props) {
uri={contentClaim.canonical_url} uri={contentClaim.canonical_url}
searchParams={{ searchParams={{
...(isChannel ? { view: 'discussion' } : {}), ...(isChannel ? { view: 'discussion' } : {}),
...(isLivestream ? {} : { lc: comment.comment_id }),
}} }}
hideActions hideActions
hideMenu hideMenu

View file

@ -385,11 +385,11 @@ export default function SettingsCreatorPage(props: Props) {
// prettier-ignore // prettier-ignore
const HELP = { const HELP = {
SLOW_MODE: 'Minimum time gap in seconds between comments (affects livestream chat as well).', SLOW_MODE: 'Minimum time gap in seconds between comments.',
MIN_TIP: 'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.', MIN_TIP: 'Enabling a minimum amount to comment will force all comments to have tips associated with them. This can help prevent spam.',
MIN_SUPER: 'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.', MIN_SUPER: 'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.',
MIN_SUPER_OFF: '(This settings is not applicable if all comments require a tip.)', MIN_SUPER_OFF: '(This settings is not applicable if all comments require a tip.)',
BLOCKED_WORDS: 'Comments and livestream chat containing these words will be blocked.', BLOCKED_WORDS: 'Comments containing these words will be blocked.',
MODERATORS: 'Moderators can block channels on your behalf. Blocked channels will appear in your "Blocked and Muted" list.', MODERATORS: 'Moderators can block channels on your behalf. Blocked channels will appear in your "Blocked and Muted" list.',
MODERATOR_SEARCH: 'Enter a channel name or URL to add as a moderator.\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8', MODERATOR_SEARCH: 'Enter a channel name or URL to add as a moderator.\nExamples:\n - @channel\n - @channel#3\n - https://odysee.com/@Odysee:8\n - lbry://@Odysee#8',
}; };

View file

@ -10,7 +10,6 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectClaimIsPending, makeSelectClaimIsPending,
makeSelectClaimIsStreamPlaceholder,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { import {
makeSelectCollectionForId, makeSelectCollectionForId,
@ -79,7 +78,6 @@ const select = (state, props) => {
title: makeSelectTitleForUri(uri)(state), title: makeSelectTitleForUri(uri)(state),
claimIsMine: makeSelectClaimIsMine(uri)(state), claimIsMine: makeSelectClaimIsMine(uri)(state),
claimIsPending: makeSelectClaimIsPending(uri)(state), claimIsPending: makeSelectClaimIsPending(uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(uri)(state),
collection: makeSelectCollectionForId(collectionId)(state), collection: makeSelectCollectionForId(collectionId)(state),
collectionId: collectionId, collectionId: collectionId,
collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state), collectionUrls: makeSelectUrlsForCollectionId(collectionId)(state),

View file

@ -1,5 +1,4 @@
// @flow // @flow
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Redirect, useHistory } from 'react-router-dom'; import { Redirect, useHistory } from 'react-router-dom';
@ -14,7 +13,6 @@ import * as COLLECTIONS_CONSTS from 'constants/collections';
import AbandonedChannelPreview from 'component/abandonedChannelPreview'; import AbandonedChannelPreview from 'component/abandonedChannelPreview';
import FilePage from 'page/file'; import FilePage from 'page/file';
import LivestreamPage from 'page/livestream';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
type Props = { type Props = {
@ -31,7 +29,6 @@ type Props = {
title: string, title: string,
claimIsMine: boolean, claimIsMine: boolean,
claimIsPending: boolean, claimIsPending: boolean,
isLivestream: boolean,
beginPublish: (?string) => void, beginPublish: (?string) => void,
collectionId: string, collectionId: string,
collection: Collection, collection: Collection,
@ -51,7 +48,6 @@ function ShowPage(props: Props) {
claimIsMine, claimIsMine,
isSubscribed, isSubscribed,
claimIsPending, claimIsPending,
isLivestream,
beginPublish, beginPublish,
fetchCollectionItems, fetchCollectionItems,
collectionId, collectionId,
@ -175,8 +171,6 @@ function ShowPage(props: Props) {
/> />
</Page> </Page>
); );
} else if (isLivestream && ENABLE_NO_SOURCE_CLAIMS) {
innerContent = <LivestreamPage uri={uri} />;
} else { } else {
innerContent = <FilePage uri={uri} location={location} />; innerContent = <FilePage uri={uri} location={location} />;
} }

View file

@ -17,7 +17,6 @@ import userReducer from 'redux/reducers/user';
import commentsReducer from 'redux/reducers/comments'; import commentsReducer from 'redux/reducers/comments';
import blockedReducer from 'redux/reducers/blocked'; import blockedReducer from 'redux/reducers/blocked';
import coinSwapReducer from 'redux/reducers/coinSwap'; import coinSwapReducer from 'redux/reducers/coinSwap';
import livestreamReducer from 'redux/reducers/livestream';
import searchReducer from 'redux/reducers/search'; import searchReducer from 'redux/reducers/search';
import reactionsReducer from 'redux/reducers/reactions'; import reactionsReducer from 'redux/reducers/reactions';
import syncReducer from 'redux/reducers/sync'; import syncReducer from 'redux/reducers/sync';
@ -34,7 +33,6 @@ export default (history) =>
content: contentReducer, content: contentReducer,
costInfo: costInfoReducer, costInfo: costInfoReducer,
fileInfo: fileInfoReducer, fileInfo: fileInfoReducer,
livestream: livestreamReducer,
notifications: notificationsReducer, notifications: notificationsReducer,
publish: publishReducer, publish: publishReducer,
reactions: reactionsReducer, reactions: reactionsReducer,

View file

@ -552,7 +552,6 @@ export function doCommentReact(commentId: string, type: string) {
* @param claim_id - File claim id * @param claim_id - File claim id
* @param parent_id - What is this? * @param parent_id - What is this?
* @param uri * @param uri
* @param livestream
* @param {string} [txid] Optional transaction id * @param {string} [txid] Optional transaction id
* @param {string} [payment_intent_id] Optional transaction id * @param {string} [payment_intent_id] Optional transaction id
* @param {string} [environment] Optional environment for Stripe (test|live) * @param {string} [environment] Optional environment for Stripe (test|live)
@ -563,7 +562,6 @@ export function doCommentCreate(
claim_id: string = '', claim_id: string = '',
parent_id?: string, parent_id?: string,
uri: string, uri: string,
livestream?: boolean = false,
txid?: string, txid?: string,
payment_intent_id?: string, payment_intent_id?: string,
environment?: string environment?: string
@ -624,7 +622,6 @@ export function doCommentCreate(
type: ACTIONS.COMMENT_CREATE_COMPLETED, type: ACTIONS.COMMENT_CREATE_COMPLETED,
data: { data: {
uri, uri,
livestream,
comment: result, comment: result,
claimId: claim_id, claimId: claim_id,
}, },

View file

@ -1,122 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { doClaimSearch } from 'redux/actions/claims';
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
export const doFetchNoSourceClaims = (channelId: string) => async (dispatch: Dispatch, getState: GetState) => {
dispatch({
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED,
data: channelId,
});
try {
await dispatch(
doClaimSearch({
channel_ids: [channelId],
has_no_source: true,
claim_type: ['stream'],
no_totals: true,
page_size: 20,
page: 1,
include_is_my_output: true,
})
);
dispatch({
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_COMPLETED,
data: channelId,
});
} catch (error) {
dispatch({
type: ACTIONS.FETCH_NO_SOURCE_CLAIMS_FAILED,
data: channelId,
});
}
};
const FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS = 5 * 60 * 1000;
export const doFetchActiveLivestreams = (
orderBy: Array<string> = ['release_time'],
pageSize: number = 50,
forceFetch: boolean = false
) => {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const now = Date.now();
const timeDelta = now - state.livestream.activeLivestreamsLastFetchedDate;
const prevOptions = state.livestream.activeLivestreamsLastFetchedOptions;
const nextOptions = { page_size: pageSize, order_by: orderBy };
const sameOptions = JSON.stringify(prevOptions) === JSON.stringify(nextOptions);
if (!forceFetch && sameOptions && timeDelta < FETCH_ACTIVE_LIVESTREAMS_MIN_INTERVAL_MS) {
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_SKIPPED });
return;
}
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED });
fetch(LIVESTREAM_LIVE_API)
.then((res) => res.json())
.then((res) => {
if (!res.data) {
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
return;
}
const activeLivestreams: LivestreamInfo = res.data.reduce((acc, curr) => {
acc[curr.claimId] = {
live: curr.live,
viewCount: curr.viewCount,
creatorId: curr.claimId,
};
return acc;
}, {});
dispatch(
// ** Creators can have multiple livestream claims (each with unique
// chat), and all of them will play the same stream when creator goes
// live. The UI usually just wants to report the latest claim, so we
// query that store it in `latestClaimUri`.
doClaimSearch({
page: 1,
page_size: nextOptions.page_size,
has_no_source: true,
channel_ids: Object.keys(activeLivestreams),
claim_type: ['stream'],
order_by: nextOptions.order_by, // **
limit_claims_per_channel: 1, // **
no_totals: true,
})
)
.then((resolveInfo) => {
Object.values(resolveInfo).forEach((x) => {
// $FlowFixMe
const channelId = x.stream.signing_channel.claim_id;
activeLivestreams[channelId] = {
...activeLivestreams[channelId],
// $FlowFixMe
latestClaimId: x.stream.claim_id,
// $FlowFixMe
latestClaimUri: x.stream.canonical_url,
};
});
dispatch({
type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED,
data: {
activeLivestreams,
activeLivestreamsLastFetchedDate: now,
activeLivestreamsLastFetchedOptions: nextOptions,
},
});
})
.catch(() => {
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
});
})
.catch((err) => {
dispatch({ type: ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED });
});
};
};

View file

@ -4,14 +4,8 @@ import * as ACTIONS from 'constants/action_types';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { batchActions } from 'util/batch-actions'; import { batchActions } from 'util/batch-actions';
import { doCheckPendingClaims } from 'redux/actions/claims'; import { doCheckPendingClaims } from 'redux/actions/claims';
import { import { selectMyClaims, selectMyChannelClaims, selectReflectingById } from 'redux/selectors/claims';
makeSelectClaimForUri, import { selectPublishFormValues, selectMyClaimForUri } from 'redux/selectors/publish';
selectMyClaims,
selectMyChannelClaims,
// selectMyClaimsWithoutChannels,
selectReflectingById,
} from 'redux/selectors/claims';
import { makeSelectPublishFormValue, selectPublishFormValues, selectMyClaimForUri } from 'redux/selectors/publish';
import { doError } from 'redux/actions/notifications'; import { doError } from 'redux/actions/notifications';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import analytics from 'analytics'; import analytics from 'analytics';
@ -21,7 +15,6 @@ import { SPEECH_STATUS, SPEECH_PUBLISH } from 'constants/speech_urls';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import { creditsToString } from 'util/format-credits'; import { creditsToString } from 'util/format-credits';
import Lbry from 'lbry'; import Lbry from 'lbry';
// import LbryFirst from 'extras/lbry-first/lbry-first';
import { isClaimNsfw } from 'util/claim'; import { isClaimNsfw } from 'util/claim';
export const NO_FILE = '---'; export const NO_FILE = '---';
@ -34,14 +27,6 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
); );
}; };
const noFileParam = !filePath || filePath === NO_FILE;
const state = getState();
const editingUri = makeSelectPublishFormValue('editingURI')(state) || '';
const remoteUrl = makeSelectPublishFormValue('remoteFileUrl')(state);
const claim = makeSelectClaimForUri(editingUri)(state) || {};
const hasSourceFile = claim.value && claim.value.source;
const redirectToLivestream = noFileParam && !hasSourceFile && !remoteUrl;
const publishSuccess = (successResponse, lbryFirstError) => { const publishSuccess = (successResponse, lbryFirstError) => {
const state = getState(); const state = getState();
const myClaims = selectMyClaims(state); const myClaims = selectMyClaims(state);
@ -87,14 +72,7 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
}) })
); );
dispatch(doCheckPendingClaims()); dispatch(doCheckPendingClaims());
// @if TARGET='app'
dispatch(doCheckReflectingFiles()); dispatch(doCheckReflectingFiles());
// @endif
// @if TARGET='web'
if (redirectToLivestream) {
dispatch(push(`/$/${PAGES.LIVESTREAM}`));
}
// @endif
}; };
const publishFail = (error) => { const publishFail = (error) => {
@ -111,15 +89,6 @@ export const doPublishDesktop = (filePath: string, preview?: boolean) => (dispat
return; return;
} }
// Redirect on web immediately because we have a file upload progress componenet
// on the publishes page. This doesn't exist on desktop so wait until we get a response
// from the SDK
// @if TARGET='web'
if (!redirectToLivestream) {
dispatch(push(`/$/${PAGES.UPLOADS}`));
}
// @endif
dispatch(doPublish(publishSuccess, publishFail)); dispatch(doPublish(publishSuccess, publishFail));
}; };
@ -381,12 +350,9 @@ export const doPublish = (success: Function, fail: Function, preview: Function)
title, title,
contentIsFree, contentIsFree,
fee, fee,
// uri,
tags, tags,
// locations, // locations,
optimize, optimize,
isLivestreamPublish,
remoteFileUrl,
} = publishData; } = publishData;
// Handle scenario where we have a claim that has the same name as a channel we are publishing with. // Handle scenario where we have a claim that has the same name as a channel we are publishing with.
@ -439,10 +405,6 @@ export const doPublish = (success: Function, fail: Function, preview: Function)
}; };
// Temporary solution to keep the same publish flow with the new tags api // Temporary solution to keep the same publish flow with the new tags api
// Eventually we will allow users to enter their own tags on publish // Eventually we will allow users to enter their own tags on publish
// `nsfw` will probably be removed
if (remoteFileUrl) {
publishPayload.remote_url = remoteFileUrl;
}
if (publishingLicense) { if (publishingLicense) {
publishPayload.license = publishingLicense; publishPayload.license = publishingLicense;
@ -490,7 +452,7 @@ export const doPublish = (success: Function, fail: Function, preview: Function)
// Only pass file on new uploads, not metadata only edits. // Only pass file on new uploads, not metadata only edits.
// The sdk will figure it out // The sdk will figure it out
if (filePath && !isLivestreamPublish) publishPayload.file_path = filePath; if (filePath) publishPayload.file_path = filePath;
if (preview) { if (preview) {
publishPayload.preview = true; publishPayload.preview = true;
@ -502,22 +464,7 @@ export const doPublish = (success: Function, fail: Function, preview: Function)
} }
return Lbry.publish(publishPayload).then((response: PublishResponse) => { return Lbry.publish(publishPayload).then((response: PublishResponse) => {
// TODO: Restore LbryFirst
// if (!useLBRYUploader) {
return success(response); return success(response);
// }
// $FlowFixMe
// publishPayload.permanent_url = response.outputs[0].permanent_url;
//
// return LbryFirst.upload(publishPayload)
// .then(() => {
// // Return original publish response so app treats it like a normal publish
// return success(response);
// })
// .catch((error) => {
// return success(response, error);
// });
}, fail); }, fail);
}; };

View file

@ -103,13 +103,6 @@ export const doCommentSocketConnect = (uri, claimId) => (dispatch) => {
data: { comment: newComment, claimId, uri }, data: { comment: newComment, claimId, uri },
}); });
} }
if (response.type === 'viewers') {
const connected = response.data.connected;
dispatch({
type: ACTIONS.VIEWERS_RECEIVED,
data: { connected, claimId },
});
}
if (response.type === 'pinned') { if (response.type === 'pinned') {
const pinnedComment = response.data.comment; const pinnedComment = response.data.comment;
dispatch({ dispatch({

View file

@ -71,12 +71,7 @@ export default handleActions(
}), }),
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => { [ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
const { const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data;
comment,
claimId,
uri,
livestream,
}: { comment: Comment, claimId: string, uri: string, livestream: boolean } = action.data;
const commentById = Object.assign({}, state.commentById); const commentById = Object.assign({}, state.commentById);
const byId = Object.assign({}, state.byId); const byId = Object.assign({}, state.byId);
@ -87,37 +82,34 @@ export default handleActions(
const comments = byId[claimId] || []; const comments = byId[claimId] || [];
const newCommentIds = comments.slice(); const newCommentIds = comments.slice();
// If it was created during a livestream, let the websocket handler perform the state update // add the comment by its ID
if (!livestream) { commentById[comment.comment_id] = comment;
// add the comment by its ID
commentById[comment.comment_id] = comment;
// push the comment_id to the top of ID list // push the comment_id to the top of ID list
newCommentIds.unshift(comment.comment_id); newCommentIds.unshift(comment.comment_id);
byId[claimId] = newCommentIds; byId[claimId] = newCommentIds;
if (totalCommentsById[claimId]) { if (totalCommentsById[claimId]) {
totalCommentsById[claimId] += 1; totalCommentsById[claimId] += 1;
}
if (comment['parent_id']) {
if (!repliesByParentId[comment.parent_id]) {
repliesByParentId[comment.parent_id] = [comment.comment_id];
} else {
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
} }
if (comment['parent_id']) { // Update the parent's "replies" value
if (!repliesByParentId[comment.parent_id]) { if (commentById[comment.parent_id]) {
repliesByParentId[comment.parent_id] = [comment.comment_id]; commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1;
} else { }
repliesByParentId[comment.parent_id].unshift(comment.comment_id); } else {
} if (!topLevelCommentsById[claimId]) {
commentsByUri[uri] = claimId;
// Update the parent's "replies" value topLevelCommentsById[claimId] = [comment.comment_id];
if (commentById[comment.parent_id]) {
commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1;
}
} else { } else {
if (!topLevelCommentsById[claimId]) { topLevelCommentsById[claimId].unshift(comment.comment_id);
commentsByUri[uri] = claimId;
topLevelCommentsById[claimId] = [comment.comment_id];
} else {
topLevelCommentsById[claimId].unshift(comment.comment_id);
}
} }
} }

View file

@ -1,61 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const defaultState: LivestreamState = {
fetchingById: {},
viewersById: {},
fetchingActiveLivestreams: false,
activeLivestreams: null,
activeLivestreamsLastFetchedDate: 0,
activeLivestreamsLastFetchedOptions: {},
};
export default handleActions(
{
[ACTIONS.FETCH_NO_SOURCE_CLAIMS_STARTED]: (state: LivestreamState, action: any): LivestreamState => {
const claimId = action.data;
const newIdsFetching = Object.assign({}, state.fetchingById);
newIdsFetching[claimId] = true;
return { ...state, fetchingById: newIdsFetching };
},
[ACTIONS.FETCH_NO_SOURCE_CLAIMS_COMPLETED]: (state: LivestreamState, action: any): LivestreamState => {
const claimId = action.data;
const newIdsFetching = Object.assign({}, state.fetchingById);
newIdsFetching[claimId] = false;
return { ...state, fetchingById: newIdsFetching };
},
[ACTIONS.FETCH_NO_SOURCE_CLAIMS_FAILED]: (state: LivestreamState, action: any) => {
const claimId = action.data;
const newIdsFetching = Object.assign({}, state.fetchingById);
newIdsFetching[claimId] = false;
return { ...state, fetchingById: newIdsFetching };
},
[ACTIONS.VIEWERS_RECEIVED]: (state: LivestreamState, action: any) => {
const { connected, claimId } = action.data;
const newViewersById = Object.assign({}, state.viewersById);
newViewersById[claimId] = connected;
return { ...state, viewersById: newViewersById };
},
[ACTIONS.FETCH_ACTIVE_LIVESTREAMS_STARTED]: (state: LivestreamState) => {
return { ...state, fetchingActiveLivestreams: true };
},
[ACTIONS.FETCH_ACTIVE_LIVESTREAMS_FAILED]: (state: LivestreamState) => {
return { ...state, fetchingActiveLivestreams: false };
},
[ACTIONS.FETCH_ACTIVE_LIVESTREAMS_COMPLETED]: (state: LivestreamState, action: any) => {
const { activeLivestreams, activeLivestreamsLastFetchedDate, activeLivestreamsLastFetchedOptions } = action.data;
return {
...state,
fetchingActiveLivestreams: false,
activeLivestreams,
activeLivestreamsLastFetchedDate,
activeLivestreamsLastFetchedOptions,
};
},
},
defaultState
);

View file

@ -1,62 +0,0 @@
// @flow
import { createSelector } from 'reselect';
import { selectMyClaims, selectPendingClaims } from 'redux/selectors/claims';
const selectState = (state) => state.livestream || {};
// select non-pending claims without sources for given channel
export const makeSelectLivestreamsForChannelId = (channelId: string) =>
createSelector(selectState, selectMyClaims, (livestreamState, myClaims = []) => {
return myClaims
.filter(
(claim) =>
claim.value_type === 'stream' &&
claim.value &&
!claim.value.source &&
claim.confirmations > 0 &&
claim.signing_channel &&
claim.signing_channel.claim_id === channelId
)
.sort((a, b) => b.timestamp - a.timestamp); // newest first
});
export const selectFetchingLivestreams = createSelector(selectState, (state) => state.fetchingById);
export const selectViewersById = createSelector(selectState, (state) => state.viewersById);
export const makeSelectIsFetchingLivestreams = (channelId: string) =>
createSelector(selectFetchingLivestreams, (fetchingLivestreams) => Boolean(fetchingLivestreams[channelId]));
export const makeSelectViewersForId = (channelId: string) =>
createSelector(selectViewersById, (viewers) => viewers[channelId]);
export const makeSelectPendingLivestreamsForChannelId = (channelId: string) =>
createSelector(selectPendingClaims, (pendingClaims) => {
return pendingClaims.filter(
(claim) =>
claim.value_type === 'stream' &&
claim.value &&
!claim.value.source &&
claim.signing_channel &&
claim.signing_channel.claim_id === channelId
);
});
export const selectActiveLivestreams = createSelector(selectState, (state) => state.activeLivestreams);
export const makeSelectIsActiveLivestream = (uri: string) =>
createSelector(selectState, (state) => {
const activeLivestreamValues = (state.activeLivestreams && Object.values(state.activeLivestreams)) || [];
// $FlowFixMe
return Boolean(activeLivestreamValues.find((v) => v.latestClaimUri === uri));
});
export const makeSelectActiveLivestreamUris = (uri: string) =>
createSelector(selectState, (state) => {
const activeLivestreamValues = (state.activeLivestreams && Object.values(state.activeLivestreams)) || [];
const uris = [];
activeLivestreamValues.forEach((v) => {
// $FlowFixMe
if (v.latestClaimUri) uris.push(v.latestClaimUri);
});
return uris;
});

View file

@ -57,7 +57,6 @@
@import 'component/superchat'; @import 'component/superchat';
@import 'component/syntax-highlighter'; @import 'component/syntax-highlighter';
@import 'component/table'; @import 'component/table';
@import 'component/livestream';
@import 'component/tabs'; @import 'component/tabs';
@import 'component/tooltip'; @import 'component/tooltip';
@import 'component/txo-list'; @import 'component/txo-list';

View file

@ -1,469 +0,0 @@
$discussion-header__height: 3rem;
$recent-msg-button__height: 2rem;
.livestream {
flex: 1;
width: 100%;
padding-top: var(--aspect-ratio-standard);
position: relative;
border-radius: var(--border-radius);
.media__thumb,
iframe {
overflow: hidden;
border-radius: var(--border-radius);
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
~ .notice-message {
margin-top: var(--spacing-m);
}
}
.livestream__discussion {
width: 100%;
@media (min-width: $breakpoint-medium) {
margin: 0;
width: var(--livestream-comments-width);
height: calc(100vh - var(--header-height));
position: fixed;
right: 0;
top: var(--header-height);
bottom: 0;
border-radius: 0;
border-top: none;
border-bottom: none;
border-right: none;
.card__main-actions {
padding: 0;
}
}
}
.livestream-discussion__header {
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--spacing-s);
margin-bottom: 0;
align-items: center;
@media (min-width: $breakpoint-small) {
height: $discussion-header__height;
padding: 0 var(--spacing-s);
padding-right: 0;
}
}
.livestream-discussion__title {
@extend .card__title-section;
@extend .card__title-section--small;
padding: 0;
}
.livestream__comments-wrapper {
display: flex;
flex-direction: column;
height: calc(100vh - var(--header-height) - #{$discussion-header__height});
}
.livestream__comments {
display: flex;
flex-direction: column-reverse;
font-size: var(--font-small);
overflow-y: scroll;
overflow-x: visible;
padding-top: var(--spacing-s);
width: 100%;
}
.livestream-comment {
list-style-type: none;
position: relative;
.channel-name {
font-size: var(--font-xsmall);
}
}
.livestream-comment--superchat {
+ .livestream-comment--superchat {
margin-bottom: var(--spacing-xxs);
}
.livestream-comment__info {
margin-top: calc(var(--spacing-xxs) / 2);
}
&::before {
position: absolute;
left: 0;
height: 100%;
max-height: 4rem;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
width: 5px;
background-color: var(--color-superchat);
content: '';
}
}
.livestream-comment__body {
display: flex;
align-items: flex-start;
}
.livestream-comment__body {
display: flex;
align-items: flex-start;
margin-left: var(--spacing-s);
.channel-thumbnail {
@include handleChannelGif(2rem);
margin-top: var(--spacing-xxs);
flex-shrink: 0;
}
}
.livestream-comment__menu {
position: absolute;
right: var(--spacing-xs);
top: var(--spacing-xs);
}
.livestream__comments__scroll-to-recent {
margin-top: -$recent-msg-button__height;
align-self: center;
margin-bottom: var(--spacing-xs);
font-size: var(--font-xsmall);
padding: var(--spacing-xxs) var(--spacing-s);
opacity: 0.9;
&:hover {
opacity: 1;
}
}
.livestream__comment-create {
padding: var(--spacing-s);
border-top: 1px solid var(--color-border);
margin-top: auto;
}
.livestream__channel-link {
margin-bottom: var(--spacing-xl);
box-shadow: 0 0 0 rgba(246, 72, 83, 0.4);
animation: livePulse 2s infinite;
&:hover {
cursor: pointer;
}
}
@keyframes livePulse {
0% {
box-shadow: 0 0 0 0 rgba(246, 72, 83, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(246, 72, 83, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(246, 72, 83, 0);
}
}
.livestream__publish-checkbox {
margin: var(--spacing-l) 0;
.checkbox,
.radio {
margin-top: var(--spacing-m);
label {
color: #444;
}
}
}
.livestream__creator-message {
background-color: #fde68a;
padding: var(--spacing-m);
color: black;
border-radius: var(--border-radius);
h4 {
font-weight: bold;
font-size: var(--font-small);
margin-bottom: var(--spacing-s);
}
}
.livestream__publish-intro {
margin-top: var(--spacing-l);
}
.livestream__viewer-count {
display: flex;
align-items: center;
.icon {
margin-left: var(--spacing-xs);
}
}
.livestream-superchats__wrapper {
flex-shrink: 0;
position: relative;
overflow-x: scroll;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background);
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-pinned__wrapper {
display: flex;
flex-shrink: 0;
position: relative;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background-highlighted);
width: 100%;
.livestream-comment {
width: 100%;
padding-top: var(--spacing-xs);
max-height: 6rem;
overflow-y: scroll;
}
.close-button {
border-left: 1px solid var(--color-border);
padding: 0 calc(var(--spacing-m) - var(--spacing-xs)) 0 var(--spacing-m);
color: var(--color-text-subtitle);
}
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-superchat__amount-large {
.credit-amount {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
}
.livestream-superchats__inner {
display: flex;
}
.livestream-superchat {
display: flex;
margin-right: var(--spacing-xs);
padding: var(--spacing-xxs);
border-radius: var(--border-radius);
.channel-thumbnail {
margin-right: var(--spacing-xs);
@include handleChannelGif(2rem);
}
&:first-of-type {
background-color: var(--color-superchat);
.channel-name {
max-width: 8rem;
}
}
&:nth-of-type(2) {
background-color: var(--color-superchat-2);
}
&:nth-of-type(3) {
background-color: var(--color-superchat-3);
}
&:nth-of-type(-n + 3) {
.channel-name,
.credit-amount {
color: var(--color-black);
}
}
.channel-name {
max-width: 5rem;
}
}
.livestream-superchat__info {
display: flex;
flex-direction: column;
justify-content: center;
font-size: var(--font-xsmall);
}
.livestream-superchat__banner {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: 0.25rem var(--spacing-s);
display: inline-block;
position: relative;
}
// This is just a two small circles that overlap to make it look like
// the banner and the left border are connected
.livestream-superchat__banner-corner {
height: calc(var(--border-radius) * 2);
width: calc(var(--border-radius) * 2);
border-radius: 50%;
position: absolute;
background-color: var(--color-superchat);
bottom: 0;
left: 0;
transform: translateX(0) translateY(50%);
&::after {
content: '';
height: calc(var(--border-radius) * 2);
width: calc(var(--border-radius) * 2);
border-top-left-radius: var(--border-radius);
background-color: var(--color-card-background);
position: absolute;
bottom: 0;
left: 0;
transform: translateX(25%) translateY(50%);
}
}
.livestream-comment__text {
padding-right: var(--spacing-xl);
padding-bottom: var(--spacing-xxs);
.markdown-preview {
p {
word-break: break-word;
}
.channel-name {
font-size: var(--font-small);
}
}
}
.livestream-superchat__tooltip-amount {
margin-top: var(--spacing-xs);
margin-left: 0;
background-color: transparent;
padding: 0;
}
.livestream__superchat-comment {
margin-top: var(--spacing-s);
max-width: 5rem;
overflow-wrap: break-word;
}
.livestream-superchat__amount-large {
min-width: 2.5rem;
}
.table--livestream-data {
td:nth-of-type(1) {
max-width: 4rem;
}
td:nth-of-type(2) {
min-width: 8.5rem;
}
td:nth-of-type(3) {
width: 4rem;
min-width: 9rem;
}
td:nth-of-type(4) {
display: none;
}
@media (min-width: $breakpoint-small) {
td:nth-of-type(1) {
max-width: 4rem;
}
td:nth-of-type(2) {
width: 40%;
}
td:nth-of-type(3) {
width: 5rem;
}
td:nth-of-type(4) {
width: 100%;
display: table-cell;
}
}
}
.livestream_thumb_container {
height: 4rem;
width: 100%;
display: flex;
flex-direction: row;
overflow: hidden;
}
.livestream___thumb {
padding: 0 var(--spacing-xxs);
object-fit: cover;
}
.livestream__data-row {
cursor: pointer;
.radio {
cursor: pointer;
}
&:nth-child(n) {
&.livestream__data-row--selected {
background-color: var(--color-button-toggle-bg);
}
}
td {
padding-right: var(--spacing-m);
@media (max-width: $breakpoint-small) {
padding: var(--spacing-xs);
}
}
&:hover {
td {
.radio {
label::before {
cursor: pointer;
background-color: var(--color-input-toggle-bg-hover);
}
}
label {
cursor: pointer;
}
}
input {
cursor: pointer;
background-color: var(--color-input-bg-selected);
}
}
}
.livestream-list--view-more {
display: flex;
align-items: flex-end;
margin-bottom: var(--spacing-m);
}

View file

@ -206,47 +206,6 @@
} }
} }
.main--livestream {
@extend .main--file-page;
margin: 0;
padding: 0;
max-width: none;
.card-stack {
margin-top: var(--spacing-m);
margin-bottom: var(--spacing-m);
@media (min-width: $breakpoint-large + 300px) {
max-width: calc(var(--page-max-width--filepage) / 1.25);
margin-left: auto;
margin-right: auto;
}
@media (min-width: $breakpoint-medium) and (max-width: $breakpoint-large + 300px) {
max-width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 3);
margin-left: var(--spacing-m);
margin-right: var(--spacing-m);
}
@media (max-width: $breakpoint-medium) {
max-width: none;
}
}
.main__right-side {
width: var(--livestream-comments-width);
@media (max-width: $breakpoint-medium) {
width: 100%;
margin-bottom: var(--spacing-m);
}
}
@media (max-width: $breakpoint-medium) {
padding: 0 var(--spacing-m);
}
}
.main--full-width { .main--full-width {
@extend .main; @extend .main;

View file

@ -97,8 +97,6 @@ $breakpoint-large: 1600px;
--file-list-thumbnail-width: 10rem; --file-list-thumbnail-width: 10rem;
--tag-height: 1.5rem; --tag-height: 1.5rem;
--livestream-comments-width: 30rem;
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {

View file

@ -1,24 +0,0 @@
// @flow
/**
* Helper to extract livestream claim uris from the output of
* `selectActiveLivestreams`.
*
* @param activeLivestreams Object obtained from `selectActiveLivestreams`.
* @param channelIds List of channel IDs to filter the results with.
* @returns {[]|Array<*>}
*/
export function getLivestreamUris(activeLivestreams: ?LivestreamInfo, channelIds: ?Array<string>) {
let values = (activeLivestreams && Object.values(activeLivestreams)) || [];
if (channelIds && channelIds.length > 0) {
// $FlowFixMe
values = values.filter((v) => channelIds.includes(v.creatorId) && Boolean(v.latestClaimUri));
} else {
// $FlowFixMe
values = values.filter((v) => Boolean(v.latestClaimUri));
}
// $FlowFixMe
return values.map((v) => v.latestClaimUri);
}