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
ui
component
channelContent
channelMentionSuggestions
claimPreview
claimPreviewSubtitle
claimPreviewTile
claimType
commentCreate
common
fileActions
fileReactions
fileSubtitle
fileTitleSection
fileType
fileViewCount
fileViewCountInline
header
livestreamComment
livestreamComments
livestreamLayout
livestreamLink
livestreamList
notification
page
publishFile
publishForm
router
constants
modal
modalPublish
modalPublishPreview
page
discover
file
livestream
livestreamCurrent
livestreamSetup
ownComments
settingsCreator
show
reducers.js
redux
scss
util

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} isLivestream={isLivestream} /> <FileViewCountInline uri={uri} />
<DateTime timeAgo uri={uri} /> <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) {
window.open(pathPlusRedirect);
} else {
push(pathPlusRedirect); 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,7 +155,6 @@ 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}`)}
@ -169,7 +164,6 @@ function FileActions(props: Props) {
{__('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,149 +322,8 @@ 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 <FileSelector
label={__('File')} label={__('File')}
disabled={disabled} disabled={disabled}
@ -623,6 +332,8 @@ function PublishFile(props: Props) {
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files // https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
placeholder={__('Select file to upload')} 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,7 +67,6 @@ 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')}
@ -85,18 +76,6 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
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 Update');
} else {
modalTitle = __('Confirm Edit'); 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,8 +82,6 @@ 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
if (!livestream) {
// add the comment by its ID // add the comment by its ID
commentById[comment.comment_id] = comment; commentById[comment.comment_id] = comment;
@ -119,7 +112,6 @@ export default handleActions(
topLevelCommentsById[claimId].unshift(comment.comment_id); topLevelCommentsById[claimId].unshift(comment.comment_id);
} }
} }
}
return { return {
...state, ...state,

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