strip out livestreams
This commit is contained in:
parent
fca18c26d3
commit
ab9f70930d
78 changed files with 126 additions and 3504 deletions
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
|
@ -13,12 +12,11 @@ type Props = {
|
||||||
pending?: boolean,
|
pending?: boolean,
|
||||||
type: string,
|
type: string,
|
||||||
beginPublish: (?string) => void,
|
beginPublish: (?string) => void,
|
||||||
isLivestream: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// previews used in channel overview and homepage (and other places?)
|
// previews used in channel overview and homepage (and other places?)
|
||||||
function ClaimPreviewSubtitle(props: Props) {
|
function ClaimPreviewSubtitle(props: Props) {
|
||||||
const { pending, uri, claim, type, beginPublish, isLivestream } = props;
|
const { pending, uri, claim, type, beginPublish } = props;
|
||||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||||
|
|
||||||
let isChannel;
|
let isChannel;
|
||||||
|
@ -38,15 +36,12 @@ function ClaimPreviewSubtitle(props: Props) {
|
||||||
type !== 'inline' &&
|
type !== 'inline' &&
|
||||||
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
||||||
|
|
||||||
{!isChannel &&
|
{!isChannel && (
|
||||||
(isLivestream && ENABLE_NO_SOURCE_CLAIMS ? (
|
<>
|
||||||
__('Livestream')
|
<FileViewCountInline uri={uri} />
|
||||||
) : (
|
<DateTime timeAgo uri={uri} />
|
||||||
<>
|
</>
|
||||||
<FileViewCountInline uri={uri} isLivestream={isLivestream} />
|
)}
|
||||||
<DateTime timeAgo uri={uri} />
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { FF_MAX_CHARS_IN_COMMENT, FF_MAX_CHARS_IN_LIVESTREAM_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
|
@ -40,7 +40,6 @@ type Props = {
|
||||||
activeChannel: string,
|
activeChannel: string,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
bottom: boolean,
|
bottom: boolean,
|
||||||
livestream?: boolean,
|
|
||||||
embed?: boolean,
|
embed?: boolean,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
supportDisabled: boolean,
|
supportDisabled: boolean,
|
||||||
|
@ -68,8 +67,6 @@ export function CommentCreate(props: Props) {
|
||||||
parentId,
|
parentId,
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
bottom,
|
bottom,
|
||||||
livestream,
|
|
||||||
embed,
|
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
settingsByChannelId,
|
settingsByChannelId,
|
||||||
supportDisabled,
|
supportDisabled,
|
||||||
|
@ -179,7 +176,6 @@ export function CommentCreate(props: Props) {
|
||||||
let newMentionValue = mentionValue.replace('lbry://', '');
|
let newMentionValue = mentionValue.replace('lbry://', '');
|
||||||
if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':');
|
if (newMentionValue.includes('#')) newMentionValue = newMentionValue.replace('#', ':');
|
||||||
|
|
||||||
if (livestream && key !== KEYCODES.TAB) setPauseQuickSend(true);
|
|
||||||
setCommentValue(
|
setCommentValue(
|
||||||
commentValue.substring(0, selectedMentionIndex) +
|
commentValue.substring(0, selectedMentionIndex) +
|
||||||
`${newMentionValue}` +
|
`${newMentionValue}` +
|
||||||
|
@ -190,7 +186,7 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
function altEnterListener(e: SyntheticKeyboardEvent<*>) {
|
||||||
if ((livestream || e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
if ((e.ctrlKey || e.metaKey) && e.keyCode === KEYCODES.ENTER) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
buttonRef.current.click();
|
buttonRef.current.click();
|
||||||
}
|
}
|
||||||
|
@ -442,17 +438,8 @@ export function CommentCreate(props: Props) {
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (embed) {
|
|
||||||
window.open(`https://odysee.com/$/${PAGES.AUTH}?redirect=/$/${PAGES.LIVESTREAM}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathPlusRedirect = `/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`;
|
const pathPlusRedirect = `/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`;
|
||||||
if (livestream) {
|
push(pathPlusRedirect);
|
||||||
window.open(pathPlusRedirect);
|
|
||||||
} else {
|
|
||||||
push(pathPlusRedirect);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||||
|
@ -518,7 +505,6 @@ export function CommentCreate(props: Props) {
|
||||||
{!advancedEditor && (
|
{!advancedEditor && (
|
||||||
<ChannelMentionSuggestions
|
<ChannelMentionSuggestions
|
||||||
uri={uri}
|
uri={uri}
|
||||||
isLivestream={livestream}
|
|
||||||
inputRef={formFieldInputRef}
|
inputRef={formFieldInputRef}
|
||||||
mentionTerm={channelMention}
|
mentionTerm={channelMention}
|
||||||
creatorUri={channelUri}
|
creatorUri={channelUri}
|
||||||
|
@ -533,9 +519,7 @@ export function CommentCreate(props: Props) {
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={isReply ? 'content_reply' : 'content_comment'}
|
||||||
label={
|
label={
|
||||||
<span className="comment-new__label-wrapper">
|
<span className="comment-new__label-wrapper">
|
||||||
{!livestream && (
|
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
|
||||||
)}
|
|
||||||
<SelectChannel tiny />
|
<SelectChannel tiny />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@ -548,7 +532,7 @@ export function CommentCreate(props: Props) {
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
onChange={handleCommentChange}
|
onChange={handleCommentChange}
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
{isSupportComment && (
|
{isSupportComment && (
|
||||||
<WalletTipAmountSelector
|
<WalletTipAmountSelector
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,7 +30,6 @@ type Props = {
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
hideRepost?: boolean,
|
hideRepost?: boolean,
|
||||||
isLivestreamClaim: boolean,
|
|
||||||
reactionsDisabled: boolean,
|
reactionsDisabled: boolean,
|
||||||
download: (string) => void,
|
download: (string) => void,
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
|
@ -50,7 +49,6 @@ function FileActions(props: Props) {
|
||||||
clearPlayingUri,
|
clearPlayingUri,
|
||||||
doToast,
|
doToast,
|
||||||
hideRepost,
|
hideRepost,
|
||||||
isLivestreamClaim,
|
|
||||||
reactionsDisabled,
|
reactionsDisabled,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
|
@ -96,7 +94,7 @@ function FileActions(props: Props) {
|
||||||
|
|
||||||
const lhsSection = (
|
const lhsSection = (
|
||||||
<>
|
<>
|
||||||
{ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} livestream={isLivestreamClaim} />}
|
{ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} />}
|
||||||
<ClaimSupportButton uri={uri} fileAction />
|
<ClaimSupportButton uri={uri} fileAction />
|
||||||
<ClaimCollectionAddButton uri={uri} fileAction />
|
<ClaimCollectionAddButton uri={uri} fileAction />
|
||||||
{!hideRepost && (
|
{!hideRepost && (
|
||||||
|
@ -124,14 +122,12 @@ function FileActions(props: Props) {
|
||||||
|
|
||||||
const rhsSection = (
|
const rhsSection = (
|
||||||
<>
|
<>
|
||||||
{/* @if TARGET='app' */}
|
|
||||||
<FileDownloadLink uri={uri} />
|
<FileDownloadLink uri={uri} />
|
||||||
{/* @endif */}
|
|
||||||
{claimIsMine && (
|
{claimIsMine && (
|
||||||
<Button
|
<Button
|
||||||
className="button--file-action"
|
className="button--file-action"
|
||||||
icon={ICONS.EDIT}
|
icon={ICONS.EDIT}
|
||||||
label={isLivestreamClaim ? __('Update') : __('Edit')}
|
label={__('Edit')}
|
||||||
navigate={`/$/${PAGES.UPLOAD}`}
|
navigate={`/$/${PAGES.UPLOAD}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
prepareEdit(claim, editUri, fileInfo);
|
prepareEdit(claim, editUri, fileInfo);
|
||||||
|
@ -147,7 +143,7 @@ function FileActions(props: Props) {
|
||||||
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
|
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(!isLivestreamClaim || !claimIsMine) && (
|
{!claimIsMine && (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
className="button--file-action"
|
className="button--file-action"
|
||||||
|
@ -159,17 +155,15 @@ function FileActions(props: Props) {
|
||||||
<Icon size={20} icon={ICONS.MORE} />
|
<Icon size={20} icon={ICONS.MORE} />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList className="menu__list">
|
<MenuList className="menu__list">
|
||||||
{!claimIsMine && (
|
<MenuItem
|
||||||
<MenuItem
|
className="comment__menu-option"
|
||||||
className="comment__menu-option"
|
onSelect={() => push(`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`)}
|
||||||
onSelect={() => push(`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`)}
|
>
|
||||||
>
|
<div className="menu__link">
|
||||||
<div className="menu__link">
|
<Icon aria-hidden icon={ICONS.REPORT} />
|
||||||
<Icon aria-hidden icon={ICONS.REPORT} />
|
{__('Report content')}
|
||||||
{__('Report content')}
|
</div>
|
||||||
</div>
|
</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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={
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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} />;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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);
|
|
|
@ -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;
|
|
|
@ -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);
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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" />;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import LivestreamList from './view';
|
|
||||||
|
|
||||||
const select = (state) => ({});
|
|
||||||
|
|
||||||
export default connect(select)(LivestreamList);
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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:
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { SITE_NAME, WEB_PUBLISH_SIZE_LIMIT_GB } from 'config';
|
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
@ -14,11 +13,6 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||||
import PublishName from 'component/publishName';
|
import PublishName from 'component/publishName';
|
||||||
import CopyableText from 'component/copyableText';
|
|
||||||
import Empty from 'component/common/empty';
|
|
||||||
import moment from 'moment';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import ReactPaginate from 'react-paginate';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
|
@ -44,12 +38,7 @@ type Props = {
|
||||||
setPublishMode: (string) => void,
|
setPublishMode: (string) => void,
|
||||||
setPrevFileText: (string) => void,
|
setPrevFileText: (string) => void,
|
||||||
header: Node,
|
header: Node,
|
||||||
livestreamData: LivestreamReplayData,
|
|
||||||
isLivestreamClaim: boolean,
|
|
||||||
checkLivestreams: (string, ?string, ?string) => void,
|
|
||||||
channelId: string,
|
channelId: string,
|
||||||
channelSignature: { signature?: string, signing_ts?: string },
|
|
||||||
isCheckingLivestreams: boolean,
|
|
||||||
setWaitForFile: (boolean) => void,
|
setWaitForFile: (boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,23 +65,10 @@ function PublishFile(props: Props) {
|
||||||
setPublishMode,
|
setPublishMode,
|
||||||
setPrevFileText,
|
setPrevFileText,
|
||||||
header,
|
header,
|
||||||
livestreamData,
|
|
||||||
isLivestreamClaim,
|
|
||||||
subtitle,
|
subtitle,
|
||||||
checkLivestreams,
|
|
||||||
channelId,
|
|
||||||
channelSignature,
|
|
||||||
isCheckingLivestreams,
|
|
||||||
setWaitForFile,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const SOURCE_NONE = 'none';
|
|
||||||
const SOURCE_SELECT = 'select';
|
|
||||||
const SOURCE_UPLOAD = 'upload';
|
|
||||||
|
|
||||||
const RECOMMENDED_BITRATE = 6000000;
|
const RECOMMENDED_BITRATE = 6000000;
|
||||||
const TV_PUBLISH_SIZE_LIMIT_BYTES = WEB_PUBLISH_SIZE_LIMIT_GB * 1073741824;
|
|
||||||
const TV_PUBLISH_SIZE_LIMIT_GB_STR = String(WEB_PUBLISH_SIZE_LIMIT_GB);
|
|
||||||
|
|
||||||
const PROCESSING_MB_PER_SECOND = 0.5;
|
const PROCESSING_MB_PER_SECOND = 0.5;
|
||||||
const MINUTES_THRESHOLD = 30;
|
const MINUTES_THRESHOLD = 30;
|
||||||
|
@ -101,35 +77,10 @@ function PublishFile(props: Props) {
|
||||||
const sizeInMB = Number(size) / 1000000;
|
const sizeInMB = Number(size) / 1000000;
|
||||||
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
|
||||||
const ffmpegAvail = ffmpegStatus.available;
|
const ffmpegAvail = ffmpegStatus.available;
|
||||||
const [oversized, setOversized] = useState(false);
|
|
||||||
const [currentFile, setCurrentFile] = useState(null);
|
const [currentFile, setCurrentFile] = useState(null);
|
||||||
const [currentFileType, setCurrentFileType] = useState(null);
|
const [currentFileType, setCurrentFileType] = useState(null);
|
||||||
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
const [optimizeAvail, setOptimizeAvail] = useState(false);
|
||||||
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
|
||||||
const UPLOAD_SIZE_MESSAGE = __(
|
|
||||||
'%SITE_NAME% uploads are limited to %limit% GB. Download the app for unrestricted publishing.',
|
|
||||||
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_GB_STR }
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileSelectorModes = [
|
|
||||||
{ label: __('Upload'), actionName: SOURCE_UPLOAD, icon: ICONS.PUBLISH },
|
|
||||||
{ label: __('Choose Replay'), actionName: SOURCE_SELECT, icon: ICONS.MENU },
|
|
||||||
{ label: __('None'), actionName: SOURCE_NONE },
|
|
||||||
];
|
|
||||||
|
|
||||||
const livestreamDataStr = JSON.stringify(livestreamData);
|
|
||||||
const hasLivestreamData = livestreamData && Boolean(livestreamData.length);
|
|
||||||
const showSourceSelector = isLivestreamClaim || (hasLivestreamData && mode === PUBLISH_MODES.FILE);
|
|
||||||
|
|
||||||
const [fileSelectSource, setFileSelectSource] = useState(
|
|
||||||
IS_WEB && showSourceSelector && name ? SOURCE_SELECT : SOURCE_UPLOAD
|
|
||||||
);
|
|
||||||
// const [showFileUpdate, setShowFileUpdate] = useState(false);
|
|
||||||
const [selectedFileIndex, setSelectedFileIndex] = useState(null);
|
|
||||||
const PAGE_SIZE = 4;
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const totalPages =
|
|
||||||
hasLivestreamData && livestreamData.length > PAGE_SIZE ? Math.ceil(livestreamData.length / PAGE_SIZE) : 1;
|
|
||||||
|
|
||||||
// Reset filePath if publish mode changed
|
// Reset filePath if publish mode changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -137,46 +88,12 @@ function PublishFile(props: Props) {
|
||||||
if (currentFileType !== 'text/markdown' && !isStillEditing) {
|
if (currentFileType !== 'text/markdown' && !isStillEditing) {
|
||||||
updatePublishForm({ filePath: '' });
|
updatePublishForm({ filePath: '' });
|
||||||
}
|
}
|
||||||
} else if (mode === PUBLISH_MODES.LIVESTREAM) {
|
|
||||||
updatePublishForm({ filePath: '' });
|
|
||||||
}
|
}
|
||||||
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
|
||||||
|
|
||||||
// set default file source to select if necessary
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasLivestreamData && isLivestreamClaim) {
|
|
||||||
setWaitForFile(true);
|
|
||||||
setFileSelectSource(SOURCE_SELECT);
|
|
||||||
} else if (isLivestreamClaim) {
|
|
||||||
setFileSelectSource(SOURCE_NONE);
|
|
||||||
}
|
|
||||||
}, [hasLivestreamData, isLivestreamClaim, setFileSelectSource]);
|
|
||||||
|
|
||||||
const normalizeUrlForProtocol = (url) => {
|
|
||||||
if (url.startsWith('https://')) {
|
|
||||||
return url;
|
|
||||||
} else {
|
|
||||||
if (url.startsWith('http://')) {
|
|
||||||
return url;
|
|
||||||
} else {
|
|
||||||
return `https://${url}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// update remoteUrl when replay selected
|
|
||||||
useEffect(() => {
|
|
||||||
const livestreamData = JSON.parse(livestreamDataStr);
|
|
||||||
if (selectedFileIndex !== null && livestreamData && livestreamData.length) {
|
|
||||||
updatePublishForm({
|
|
||||||
remoteFileUrl: normalizeUrlForProtocol(livestreamData[selectedFileIndex].data.fileLocation),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedFileIndex, updatePublishForm, livestreamDataStr]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filePath || filePath === '') {
|
if (!filePath || filePath === '') {
|
||||||
setCurrentFile('');
|
setCurrentFile('');
|
||||||
setOversized(false);
|
|
||||||
updateFileInfo(0, 0, false);
|
updateFileInfo(0, 0, false);
|
||||||
} else if (typeof filePath !== 'string') {
|
} else if (typeof filePath !== 'string') {
|
||||||
// Update currentFile file
|
// Update currentFile file
|
||||||
|
@ -198,10 +115,6 @@ function PublishFile(props: Props) {
|
||||||
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePaginateReplays(page) {
|
|
||||||
setCurrentPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBitrate(size, duration) {
|
function getBitrate(size, duration) {
|
||||||
const s = Number(size);
|
const s = Number(size);
|
||||||
const d = Number(duration);
|
const d = Number(duration);
|
||||||
|
@ -236,16 +149,6 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUploadMessage() {
|
function getUploadMessage() {
|
||||||
// @if TARGET='web'
|
|
||||||
if (oversized) {
|
|
||||||
return (
|
|
||||||
<p className="help--error">
|
|
||||||
{UPLOAD_SIZE_MESSAGE}{' '}
|
|
||||||
<Button button="link" label={__('Upload Guide')} href="https://lbry.com/faq/video-publishing-guide" />
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// @endif
|
|
||||||
if (isVid && duration && getBitrate(size, duration) > RECOMMENDED_BITRATE) {
|
if (isVid && duration && getBitrate(size, duration) > RECOMMENDED_BITRATE) {
|
||||||
return (
|
return (
|
||||||
<p className="help--warning">
|
<p className="help--warning">
|
||||||
|
@ -267,32 +170,13 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!isStillEditing && name) {
|
if (!!isStillEditing && name) {
|
||||||
if (isLivestreamClaim) {
|
|
||||||
return (
|
|
||||||
<p className="help">{__('You can upload your own recording or select a replay when your stream is over')}</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
|
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// @if TARGET='web'
|
|
||||||
if (!isStillEditing) {
|
|
||||||
return (
|
|
||||||
<p className="help">
|
|
||||||
{__(
|
|
||||||
'For video content, use MP4s in H264/AAC format and a friendly bitrate (under 5 Mbps) and resolution (720p) for more reliable streaming. %SITE_NAME% uploads are restricted to %limit% GB.',
|
|
||||||
{ SITE_NAME, limit: TV_PUBLISH_SIZE_LIMIT_GB_STR }
|
|
||||||
)}{' '}
|
|
||||||
<Button button="link" label={__('Upload Guide')} href="https://lbry.com/faq/video-publishing-guide" />
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
// @if TARGET='app'
|
|
||||||
if (!isStillEditing) {
|
if (!isStillEditing) {
|
||||||
return (
|
return (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
|
@ -303,7 +187,6 @@ function PublishFile(props: Props) {
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// @endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseName(newName) {
|
function parseName(newName) {
|
||||||
|
@ -311,27 +194,6 @@ function PublishFile(props: Props) {
|
||||||
return newName.replace(INVALID_URI_CHARS, '-');
|
return newName.replace(INVALID_URI_CHARS, '-');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSource(source) {
|
|
||||||
if (source === SOURCE_NONE) {
|
|
||||||
// clear files and remotes...
|
|
||||||
// https://github.com/lbryio/lbry-desktop/issues/5855
|
|
||||||
// publish is trying to use one field to share html file blob and string and such
|
|
||||||
// $FlowFixMe
|
|
||||||
handleFileChange(false, false);
|
|
||||||
updatePublishForm({ remoteFileUrl: undefined });
|
|
||||||
} else if (source === SOURCE_UPLOAD) {
|
|
||||||
updatePublishForm({ remoteFileUrl: undefined });
|
|
||||||
} else if (source === SOURCE_SELECT) {
|
|
||||||
// $FlowFixMe
|
|
||||||
handleFileChange(false, false);
|
|
||||||
if (selectedFileIndex !== null) {
|
|
||||||
updatePublishForm({ remoteFileUrl: livestreamData[selectedFileIndex].data.fileLocation });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFileSelectSource(source);
|
|
||||||
setWaitForFile(source !== SOURCE_NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTitleChange(event) {
|
function handleTitleChange(event) {
|
||||||
const title = event.target.value;
|
const title = event.target.value;
|
||||||
// Update title
|
// Update title
|
||||||
|
@ -348,9 +210,7 @@ function PublishFile(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileChange(file: WebFile, clearName = true) {
|
function handleFileChange(file: WebFile, clearName = true) {
|
||||||
const { showToast } = props;
|
|
||||||
window.URL = window.URL || window.webkitURL;
|
window.URL = window.URL || window.webkitURL;
|
||||||
setOversized(false);
|
|
||||||
|
|
||||||
// select file, start to select a new one, then cancel
|
// select file, start to select a new one, then cancel
|
||||||
if (!file) {
|
if (!file) {
|
||||||
|
@ -410,16 +270,6 @@ function PublishFile(props: Props) {
|
||||||
setPublishMode(PUBLISH_MODES.FILE);
|
setPublishMode(PUBLISH_MODES.FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @if TARGET='web'
|
|
||||||
// we only need to enforce file sizes on 'web'
|
|
||||||
if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT_BYTES) {
|
|
||||||
setOversized(true);
|
|
||||||
showToast(__(UPLOAD_SIZE_MESSAGE));
|
|
||||||
updatePublishForm({ filePath: '', name: '' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = {
|
||||||
// if electron, we'll set filePath to the path string because SDK is handling publishing.
|
// if electron, we'll set filePath to the path string because SDK is handling publishing.
|
||||||
// File.path will be undefined from web due to browser security, so it will default to the File Object.
|
// File.path will be undefined from web due to browser security, so it will default to the File Object.
|
||||||
|
@ -472,157 +322,18 @@ function PublishFile(props: Props) {
|
||||||
value={title}
|
value={title}
|
||||||
onChange={handleTitleChange}
|
onChange={handleTitleChange}
|
||||||
/>
|
/>
|
||||||
{/* Decide whether to show file upload or replay selector */}
|
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
<>
|
|
||||||
{showSourceSelector && (
|
|
||||||
<fieldset-section>
|
|
||||||
<div className="section__actions--between section__actions--align-bottom">
|
|
||||||
<div>
|
|
||||||
<label>{__('Replay video available')}</label>
|
|
||||||
<div className="button-group">
|
|
||||||
{fileSelectorModes.map((fmode) => (
|
|
||||||
<Button
|
|
||||||
key={fmode.label}
|
|
||||||
icon={fmode.icon || undefined}
|
|
||||||
iconSize={18}
|
|
||||||
label={fmode.label}
|
|
||||||
button="alt"
|
|
||||||
onClick={() => {
|
|
||||||
// $FlowFixMe
|
|
||||||
handleFileSource(fmode.actionName);
|
|
||||||
}}
|
|
||||||
className={classnames('button-toggle', {
|
|
||||||
'button-toggle--active': fileSelectSource === fmode.actionName,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{fileSelectSource === SOURCE_SELECT && (
|
|
||||||
<Button
|
|
||||||
button="secondary"
|
|
||||||
label={__('Check for Replays')}
|
|
||||||
disabled={isCheckingLivestreams}
|
|
||||||
icon={ICONS.REFRESH}
|
|
||||||
onClick={() =>
|
|
||||||
checkLivestreams(channelId, channelSignature.signature, channelSignature.signing_ts)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fileSelectSource === SOURCE_UPLOAD && showFileUpload && (
|
|
||||||
<>
|
|
||||||
<FileSelector
|
|
||||||
label={__('File')}
|
|
||||||
disabled={disabled}
|
|
||||||
currentPath={currentFile}
|
|
||||||
onFileChosen={handleFileChange}
|
|
||||||
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
|
||||||
placeholder={__('Select a file to upload')}
|
|
||||||
/>
|
|
||||||
{getUploadMessage()}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{fileSelectSource === SOURCE_SELECT && showFileUpload && hasLivestreamData && !isCheckingLivestreams && (
|
|
||||||
<>
|
|
||||||
<fieldset-section>
|
|
||||||
<label>{__('Select Replay')}</label>
|
|
||||||
<div className="table__wrapper">
|
|
||||||
<table className="table table--livestream-data">
|
|
||||||
<tbody>
|
|
||||||
{livestreamData.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE).map((item, i) => (
|
|
||||||
<tr
|
|
||||||
onClick={() => setSelectedFileIndex((currentPage - 1) * PAGE_SIZE + i)}
|
|
||||||
key={item.id}
|
|
||||||
className={classnames('livestream__data-row', {
|
|
||||||
'livestream__data-row--selected': selectedFileIndex === (currentPage - 1) * PAGE_SIZE + i,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<FormField
|
|
||||||
type="radio"
|
|
||||||
checked={selectedFileIndex === (currentPage - 1) * PAGE_SIZE + i}
|
|
||||||
label={null}
|
|
||||||
onClick={() => setSelectedFileIndex((currentPage - 1) * PAGE_SIZE + i)}
|
|
||||||
className="livestream__data-row-radio"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="livestream_thumb_container">
|
|
||||||
{item.data.thumbnails.slice(0, 3).map((thumb) => (
|
|
||||||
<img key={thumb} className="livestream___thumb" src={thumb} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{`${Math.floor(item.data.fileDuration / 60)} ${
|
|
||||||
Math.floor(item.data.fileDuration / 60) > 1 ? __('minutes') : __('minute')
|
|
||||||
}`}
|
|
||||||
<div className="table__item-label">
|
|
||||||
{`${moment(item.data.uploadedAt).from(moment())}`}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<CopyableText
|
|
||||||
primaryButton
|
|
||||||
copyable={normalizeUrlForProtocol(item.data.fileLocation)}
|
|
||||||
snackMessage={__('Url copied.')}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</fieldset-section>
|
|
||||||
<fieldset-group class="fieldset-group--smushed fieldgroup--paginate">
|
|
||||||
<fieldset-section>
|
|
||||||
<ReactPaginate
|
|
||||||
pageCount={totalPages}
|
|
||||||
pageRangeDisplayed={2}
|
|
||||||
previousLabel="‹"
|
|
||||||
nextLabel="›"
|
|
||||||
activeClassName="pagination__item--selected"
|
|
||||||
pageClassName="pagination__item"
|
|
||||||
previousClassName="pagination__item pagination__item--previous"
|
|
||||||
nextClassName="pagination__item pagination__item--next"
|
|
||||||
breakClassName="pagination__item pagination__item--break"
|
|
||||||
marginPagesDisplayed={2}
|
|
||||||
onPageChange={(e) => handlePaginateReplays(e.selected + 1)}
|
|
||||||
forcePage={currentPage - 1}
|
|
||||||
initialPage={currentPage - 1}
|
|
||||||
containerClassName="pagination"
|
|
||||||
/>
|
|
||||||
</fieldset-section>
|
|
||||||
</fieldset-group>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{fileSelectSource === SOURCE_SELECT && showFileUpload && !hasLivestreamData && !isCheckingLivestreams && (
|
|
||||||
<div className="main--empty empty">
|
|
||||||
<Empty text={__('No replays found.')} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{fileSelectSource === SOURCE_SELECT && showFileUpload && isCheckingLivestreams && (
|
|
||||||
<div className="main--empty empty">
|
|
||||||
<Spinner small />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
{/* @endif */}
|
|
||||||
{/* @if TARGET='app' */}
|
|
||||||
{showFileUpload && (
|
{showFileUpload && (
|
||||||
<FileSelector
|
<>
|
||||||
label={__('File')}
|
<FileSelector
|
||||||
disabled={disabled}
|
label={__('File')}
|
||||||
currentPath={currentFile}
|
disabled={disabled}
|
||||||
onFileChosen={handleFileChange}
|
currentPath={currentFile}
|
||||||
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
onFileChosen={handleFileChange}
|
||||||
placeholder={__('Select file to upload')}
|
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||||
/>
|
placeholder={__('Select file to upload')}
|
||||||
|
/>
|
||||||
|
{getUploadMessage()}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{showFileUpload && (
|
{showFileUpload && (
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -658,7 +369,6 @@ function PublishFile(props: Props) {
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
|
||||||
{isPublishPost && (
|
{isPublishPost && (
|
||||||
<PostEditor
|
<PostEditor
|
||||||
label={__('Post --[noun, markdown post tab button]--')}
|
label={__('Post --[noun, markdown post tab button]--')}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
|
|
@ -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',
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export const FILE = 'file';
|
export const FILE = 'file';
|
||||||
export const POST = 'post';
|
export const POST = 'post';
|
||||||
export const LIVESTREAM = 'livestream';
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -15,7 +15,6 @@ type Props = {
|
||||||
isEdit: boolean,
|
isEdit: boolean,
|
||||||
filePath: ?string,
|
filePath: ?string,
|
||||||
lbryFirstError: ?string,
|
lbryFirstError: ?string,
|
||||||
claim: Claim,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalPublishSuccess extends React.PureComponent<Props> {
|
class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
|
@ -24,13 +23,10 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
clearPublish();
|
clearPublish();
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
const { closeModal, clearPublish, navigate, uri, isEdit, filePath, lbryFirstError, claim } = this.props;
|
const { closeModal, clearPublish, navigate, uri, isEdit, filePath, lbryFirstError } = this.props;
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const livestream = claim && claim.value && claim.value_type === 'stream' && !claim.value.source;
|
|
||||||
let contentLabel;
|
let contentLabel;
|
||||||
if (livestream) {
|
if (isEdit) {
|
||||||
contentLabel = __('Livestream Created');
|
|
||||||
} else if (isEdit) {
|
|
||||||
contentLabel = __('Update published');
|
contentLabel = __('Update published');
|
||||||
} else {
|
} else {
|
||||||
contentLabel = __('File published');
|
contentLabel = __('File published');
|
||||||
|
@ -39,10 +35,6 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
let publishMessage;
|
let publishMessage;
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
publishMessage = __('Your update is now pending. It will take a few minutes to appear for other users.');
|
publishMessage = __('Your update is now pending. It will take a few minutes to appear for other users.');
|
||||||
} else if (livestream) {
|
|
||||||
publishMessage = __(
|
|
||||||
'Your livestream is now pending. You will be able to start shortly at the streaming dashboard.'
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
publishMessage = __('Your content will be live shortly.');
|
publishMessage = __('Your content will be live shortly.');
|
||||||
}
|
}
|
||||||
|
@ -54,7 +46,7 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen type="card" contentLabel={__(contentLabel)} onAborted={handleClose}>
|
<Modal isOpen type="card" contentLabel={__(contentLabel)} onAborted={handleClose}>
|
||||||
<Card
|
<Card
|
||||||
title={livestream ? __('Livestream Created') : __('Success')}
|
title={__('Success')}
|
||||||
subtitle={publishMessage}
|
subtitle={publishMessage}
|
||||||
body={
|
body={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -75,28 +67,15 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
{!livestream && (
|
<Button
|
||||||
<Button
|
button="primary"
|
||||||
button="primary"
|
label={__('View My Uploads')}
|
||||||
label={__('View My Uploads')}
|
onClick={() => {
|
||||||
onClick={() => {
|
clearPublish();
|
||||||
clearPublish();
|
navigate(`/$/${PAGES.UPLOADS}`);
|
||||||
navigate(`/$/${PAGES.UPLOADS}`);
|
closeModal();
|
||||||
closeModal();
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{livestream && (
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
label={__('View My Dashboard')}
|
|
||||||
onClick={() => {
|
|
||||||
clearPublish();
|
|
||||||
navigate(`/$/${PAGES.LIVESTREAM}`);
|
|
||||||
closeModal();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button button="link" label={__('Close')} onClick={handleClose} />
|
<Button button="link" label={__('Close')} onClick={handleClose} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,6 @@ type Props = {
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
publishSuccess: boolean,
|
publishSuccess: boolean,
|
||||||
publishing: boolean,
|
publishing: boolean,
|
||||||
isLivestreamClaim: boolean,
|
|
||||||
remoteFile: string,
|
remoteFile: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -75,12 +74,9 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
setEnablePublishPreview,
|
setEnablePublishPreview,
|
||||||
isStillEditing,
|
isStillEditing,
|
||||||
myChannels,
|
myChannels,
|
||||||
publishSuccess,
|
|
||||||
publishing,
|
publishing,
|
||||||
publish,
|
publish,
|
||||||
closeModal,
|
closeModal,
|
||||||
isLivestreamClaim,
|
|
||||||
remoteFile,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const maxCharsBeforeOverflow = 128;
|
const maxCharsBeforeOverflow = 128;
|
||||||
|
@ -99,20 +95,7 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
return uri;
|
return uri;
|
||||||
}, [uri]);
|
}, [uri]);
|
||||||
|
|
||||||
const livestream =
|
|
||||||
(uri && isLivestreamClaim) ||
|
|
||||||
// $FlowFixMe
|
|
||||||
(previewResponse.outputs[0] && previewResponse.outputs[0].value && !previewResponse.outputs[0].value.source);
|
|
||||||
// leave the confirm modal up if we're not going straight to upload/reflecting
|
// leave the confirm modal up if we're not going straight to upload/reflecting
|
||||||
// @if TARGET='web'
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (publishing && !livestream) {
|
|
||||||
closeModal();
|
|
||||||
} else if (publishSuccess) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
}, [publishSuccess, publishing, livestream]);
|
|
||||||
// @endif
|
|
||||||
function onConfirmed() {
|
function onConfirmed() {
|
||||||
// Publish for real:
|
// Publish for real:
|
||||||
publish(getFilePathName(filePath), false);
|
publish(getFilePathName(filePath), false);
|
||||||
|
@ -147,13 +130,7 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
|
||||||
let modalTitle;
|
let modalTitle;
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
if (livestream) {
|
modalTitle = __('Confirm Edit');
|
||||||
modalTitle = __('Confirm Update');
|
|
||||||
} else {
|
|
||||||
modalTitle = __('Confirm Edit');
|
|
||||||
}
|
|
||||||
} else if (livestream) {
|
|
||||||
modalTitle = __('Create Livestream');
|
|
||||||
} else {
|
} else {
|
||||||
modalTitle = __('Confirm Upload');
|
modalTitle = __('Confirm Upload');
|
||||||
}
|
}
|
||||||
|
@ -162,16 +139,12 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
if (!publishing) {
|
if (!publishing) {
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
confirmBtnText = __('Save');
|
confirmBtnText = __('Save');
|
||||||
} else if (livestream) {
|
|
||||||
confirmBtnText = __('Create');
|
|
||||||
} else {
|
} else {
|
||||||
confirmBtnText = __('Upload');
|
confirmBtnText = __('Upload');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isStillEditing) {
|
if (isStillEditing) {
|
||||||
confirmBtnText = __('Saving');
|
confirmBtnText = __('Saving');
|
||||||
} else if (livestream) {
|
|
||||||
confirmBtnText = __('Creating');
|
|
||||||
} else {
|
} else {
|
||||||
confirmBtnText = __('Uploading');
|
confirmBtnText = __('Uploading');
|
||||||
}
|
}
|
||||||
|
@ -240,8 +213,7 @@ const ModalPublishPreview = (props: Props) => {
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<table className="table table--condensed table--publish-preview">
|
<table className="table table--condensed table--publish-preview">
|
||||||
<tbody>
|
<tbody>
|
||||||
{!livestream && !isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
|
{!isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
|
||||||
{livestream && remoteFile && createRow(__('Replay'), __('Remote File Selected'))}
|
|
||||||
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
|
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
|
||||||
{createRow(__('Title'), formattedTitle)}
|
{createRow(__('Title'), formattedTitle)}
|
||||||
{createRow(__('Description'), descriptionValue)}
|
{createRow(__('Description'), descriptionValue)}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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} />}
|
||||||
|
|
|
@ -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);
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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);
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -71,12 +71,7 @@ export default handleActions(
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
||||||
const {
|
const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data;
|
||||||
comment,
|
|
||||||
claimId,
|
|
||||||
uri,
|
|
||||||
livestream,
|
|
||||||
}: { comment: Comment, claimId: string, uri: string, livestream: boolean } = action.data;
|
|
||||||
|
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
|
@ -87,37 +82,34 @@ export default handleActions(
|
||||||
const comments = byId[claimId] || [];
|
const comments = byId[claimId] || [];
|
||||||
const newCommentIds = comments.slice();
|
const newCommentIds = comments.slice();
|
||||||
|
|
||||||
// If it was created during a livestream, let the websocket handler perform the state update
|
// add the comment by its ID
|
||||||
if (!livestream) {
|
commentById[comment.comment_id] = comment;
|
||||||
// add the comment by its ID
|
|
||||||
commentById[comment.comment_id] = comment;
|
|
||||||
|
|
||||||
// push the comment_id to the top of ID list
|
// push the comment_id to the top of ID list
|
||||||
newCommentIds.unshift(comment.comment_id);
|
newCommentIds.unshift(comment.comment_id);
|
||||||
byId[claimId] = newCommentIds;
|
byId[claimId] = newCommentIds;
|
||||||
|
|
||||||
if (totalCommentsById[claimId]) {
|
if (totalCommentsById[claimId]) {
|
||||||
totalCommentsById[claimId] += 1;
|
totalCommentsById[claimId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment['parent_id']) {
|
||||||
|
if (!repliesByParentId[comment.parent_id]) {
|
||||||
|
repliesByParentId[comment.parent_id] = [comment.comment_id];
|
||||||
|
} else {
|
||||||
|
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment['parent_id']) {
|
// Update the parent's "replies" value
|
||||||
if (!repliesByParentId[comment.parent_id]) {
|
if (commentById[comment.parent_id]) {
|
||||||
repliesByParentId[comment.parent_id] = [comment.comment_id];
|
commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1;
|
||||||
} else {
|
}
|
||||||
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
|
} else {
|
||||||
}
|
if (!topLevelCommentsById[claimId]) {
|
||||||
|
commentsByUri[uri] = claimId;
|
||||||
// Update the parent's "replies" value
|
topLevelCommentsById[claimId] = [comment.comment_id];
|
||||||
if (commentById[comment.parent_id]) {
|
|
||||||
commentById[comment.parent_id].replies = (commentById[comment.parent_id].replies || 0) + 1;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!topLevelCommentsById[claimId]) {
|
topLevelCommentsById[claimId].unshift(comment.comment_id);
|
||||||
commentsByUri[uri] = claimId;
|
|
||||||
topLevelCommentsById[claimId] = [comment.comment_id];
|
|
||||||
} else {
|
|
||||||
topLevelCommentsById[claimId].unshift(comment.comment_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
Loading…
Reference in a new issue