diff --git a/package.json b/package.json index 3ef75ace3..b25a2e60f 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "electron-is-dev": "^0.3.0", "electron-webpack": "^2.8.2", "electron-window-state": "^4.1.1", + "emoji-dictionary": "^1.0.11", "eslint": "^5.15.2", "eslint-config-prettier": "^2.9.0", "eslint-config-standard": "^12.0.0", diff --git a/static/app-strings.json b/static/app-strings.json index 3bfe3ce15..17651116a 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1181,6 +1181,7 @@ "Try out the app!": "Try out the app!", "Download the app to track files you've viewed and downloaded.": "Download the app to track files you've viewed and downloaded.", "Create a new channel": "Create a new channel", + "Go Live": "Go Live", "Thumbnail source": "Thumbnail source", "Cover source": "Cover source", "Your changes will be live in a few minutes": "Your changes will be live in a few minutes", @@ -1672,5 +1673,8 @@ "Receive emails about the latest rewards that are available to LBRY users.": "Receive emails about the latest rewards that are available to LBRY users.", "Stay up to date on the latest content from your favorite creators.": "Stay up to date on the latest content from your favorite creators.", "Receive tutorial emails related to LBRY": "Receive tutorial emails related to LBRY", + "Create A LiveStream": "Create A LiveStream", + "%channel% isn't live right now, but the chat is! Check back later to watch the stream.": "%channel% isn't live right now, but the chat is! Check back later to watch the stream.", + "Right now": "Right now", "--end--": "--end--" } diff --git a/ui/component/channelContent/view.jsx b/ui/component/channelContent/view.jsx index 85d6e5568..b026bc2d9 100644 --- a/ui/component/channelContent/view.jsx +++ b/ui/component/channelContent/view.jsx @@ -9,6 +9,7 @@ import Button from 'component/button'; import ClaimListDiscover from 'component/claimListDiscover'; import Ads from 'web/component/ads'; import Icon from 'component/common/icon'; +import LivestreamLink from 'component/livestreamLink'; import { Form, FormField } from 'component/common/form'; import { DEBOUNCE_WAIT_DURATION_MS } from 'constants/search'; import { lighthouse } from 'redux/actions/search'; @@ -106,6 +107,8 @@ function ChannelContent(props: Props) { )} + + {!fetching && channelIsBlackListed && (

diff --git a/ui/component/channelSelector/view.jsx b/ui/component/channelSelector/view.jsx index 9ceec76ee..9fd5558d8 100644 --- a/ui/component/channelSelector/view.jsx +++ b/ui/component/channelSelector/view.jsx @@ -30,7 +30,7 @@ function ChannelListItem(props: ListItemProps) { return (

- + {isSelected && }
diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index 458359d12..ea1c8543f 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -223,7 +223,7 @@ function ClaimListDiscover(props: Props) { }; if (!ENABLE_NO_SOURCE_CLAIMS) { - // options.has_source = true; + options.has_source = true; } if (feeAmountParam && claimType !== CS.CLAIM_CHANNEL) { diff --git a/ui/component/claimPreviewSubtitle/index.js b/ui/component/claimPreviewSubtitle/index.js index 78a458c07..47fe616fd 100644 --- a/ui/component/claimPreviewSubtitle/index.js +++ b/ui/component/claimPreviewSubtitle/index.js @@ -1,16 +1,23 @@ import * as PAGES from 'constants/pages'; import { connect } from 'react-redux'; -import { makeSelectClaimForUri, makeSelectClaimIsPending, doClearPublish, doPrepareEdit } from 'lbry-redux'; +import { + makeSelectClaimForUri, + makeSelectClaimIsPending, + doClearPublish, + doPrepareEdit, + makeSelectClaimHasSource, +} from 'lbry-redux'; import { push } from 'connected-react-router'; import ClaimPreviewSubtitle from './view'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), pending: makeSelectClaimIsPending(props.uri)(state), + isLivestream: !makeSelectClaimHasSource(props.uri)(state), }); -const perform = dispatch => ({ - beginPublish: name => { +const perform = (dispatch) => ({ + beginPublish: (name) => { dispatch(doClearPublish()); dispatch(doPrepareEdit({ name })); dispatch(push(`/$/${PAGES.UPLOAD}`)); diff --git a/ui/component/claimPreviewSubtitle/view.jsx b/ui/component/claimPreviewSubtitle/view.jsx index aa9748e88..26512bfb3 100644 --- a/ui/component/claimPreviewSubtitle/view.jsx +++ b/ui/component/claimPreviewSubtitle/view.jsx @@ -1,4 +1,5 @@ // @flow +import { ENABLE_NO_SOURCE_CLAIMS } from 'config'; import React from 'react'; import UriIndicator from 'component/uriIndicator'; import DateTime from 'component/dateTime'; @@ -11,10 +12,11 @@ type Props = { pending?: boolean, type: string, beginPublish: (string) => void, + isLivestream: boolean, }; function ClaimPreviewSubtitle(props: Props) { - const { pending, uri, claim, type, beginPublish } = props; + const { pending, uri, claim, type, beginPublish, isLivestream } = props; const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; let isChannel; @@ -28,13 +30,16 @@ function ClaimPreviewSubtitle(props: Props) { {claim ? ( {' '} - {!pending && - claim && - (isChannel ? ( - type !== 'inline' && `${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}` - ) : ( - - ))} + {!pending && claim && ( + <> + {isChannel && + type !== 'inline' && + `${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`} + + {!isChannel && + (isLivestream && ENABLE_NO_SOURCE_CLAIMS ? __('Livestream') : )} + + )} ) : ( diff --git a/ui/component/claimTilesDiscover/view.jsx b/ui/component/claimTilesDiscover/view.jsx index e96670174..af4e6962b 100644 --- a/ui/component/claimTilesDiscover/view.jsx +++ b/ui/component/claimTilesDiscover/view.jsx @@ -92,7 +92,7 @@ function ClaimTilesDiscover(props: Props) { }; if (!ENABLE_NO_SOURCE_CLAIMS) { - // options.has_source = true; + options.has_source = true; } if (releaseTime) { diff --git a/ui/component/commentCreate/index.js b/ui/component/commentCreate/index.js index 6644cae2a..35e1cb734 100644 --- a/ui/component/commentCreate/index.js +++ b/ui/component/commentCreate/index.js @@ -1,10 +1,16 @@ import { connect } from 'react-redux'; -import { makeSelectClaimForUri, selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; +import { + makeSelectClaimForUri, + makeSelectClaimIsMine, + selectMyChannelClaims, + selectFetchingMyChannels, +} from 'lbry-redux'; import { selectIsPostingComment } from 'redux/selectors/comments'; import { doOpenModal, doSetActiveChannel } from 'redux/actions/app'; import { doCommentCreate } from 'redux/actions/comments'; import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectActiveChannelClaim } from 'redux/selectors/app'; +import { doToast } from 'redux/actions/notifications'; import { CommentCreate } from './view'; const select = (state, props) => ({ @@ -14,12 +20,15 @@ const select = (state, props) => ({ isFetchingChannels: selectFetchingMyChannels(state), isPostingComment: selectIsPostingComment(state), activeChannelClaim: selectActiveChannelClaim(state), + claimIsMine: makeSelectClaimIsMine(props.uri)(state), }); const perform = (dispatch, ownProps) => ({ - createComment: (comment, claimId, parentId) => dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri)), + createComment: (comment, claimId, parentId) => + dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)), - setActiveChannel: claimId => dispatch(doSetActiveChannel(claimId)), + setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), + toast: (message) => dispatch(doToast({ message, isError: true })), }); export default connect(select, perform)(CommentCreate); diff --git a/ui/component/commentCreate/view.jsx b/ui/component/commentCreate/view.jsx index 9b68e2d8e..53f20d0d8 100644 --- a/ui/component/commentCreate/view.jsx +++ b/ui/component/commentCreate/view.jsx @@ -10,6 +10,16 @@ import usePersistedState from 'effects/use-persisted-state'; import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field'; import { useHistory } from 'react-router'; import type { ElementRef } from 'react'; +import emoji from 'emoji-dictionary'; + +const COMMENT_SLOW_MODE_SECONDS = 5; +const LIVESTREAM_EMOJIS = [ + emoji.getUnicode('rocket'), + emoji.getUnicode('jeans'), + emoji.getUnicode('fire'), + emoji.getUnicode('heart'), + emoji.getUnicode('open_mouth'), +]; type Props = { uri: string, @@ -25,6 +35,9 @@ type Props = { isPostingComment: boolean, activeChannel: string, activeChannelClaim: ?ChannelClaim, + livestream?: boolean, + toast: (string) => void, + claimIsMine: boolean, }; export function CommentCreate(props: Props) { @@ -40,11 +53,15 @@ export function CommentCreate(props: Props) { parentId, isPostingComment, activeChannelClaim, + livestream, + toast, + claimIsMine, } = props; const buttonref: ElementRef = React.useRef(); const { push } = useHistory(); const { claim_id: claimId } = claim; const [commentValue, setCommentValue] = React.useState(''); + const [lastCommentTime, setLastCommentTime] = React.useState(); const [charCount, setCharCount] = useState(commentValue.length); const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false); const hasChannels = channels && channels.length; @@ -79,9 +96,21 @@ export function CommentCreate(props: Props) { function handleSubmit() { if (activeChannelClaim && commentValue.length) { - createComment(commentValue, claimId, parentId).then(res => { + const timeUntilCanComment = !lastCommentTime + ? 0 + : (lastCommentTime - Date.now()) / 1000 + COMMENT_SLOW_MODE_SECONDS; + + if (livestream && !claimIsMine && timeUntilCanComment > 0) { + toast( + __('Slowmode is on. You can comment again in %time% seconds.', { time: Math.ceil(timeUntilCanComment) }) + ); + return; + } + + createComment(commentValue, claimId, parentId).then((res) => { if (res && res.signature) { setCommentValue(''); + setLastCommentTime(Date.now()); if (onDoneReplying) { onDoneReplying(); @@ -144,6 +173,23 @@ export function CommentCreate(props: Props) { autoFocus={isReply} textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT} /> + {livestream && hasChannels && ( +
+ {LIVESTREAM_EMOJIS.map((emoji) => ( +
+ )}
+ ))} + + ) : ( +
+ )} +
+ +
+ +
+ + } + /> + ); +} diff --git a/ui/component/livestreamLayout/index.js b/ui/component/livestreamLayout/index.js new file mode 100644 index 000000000..f6fe364e8 --- /dev/null +++ b/ui/component/livestreamLayout/index.js @@ -0,0 +1,10 @@ +import { connect } from 'react-redux'; +import { makeSelectClaimForUri, makeSelectThumbnailForUri } from 'lbry-redux'; +import LivestreamLayout from './view'; + +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), + thumbnail: makeSelectThumbnailForUri(props.uri)(state), +}); + +export default connect(select)(LivestreamLayout); diff --git a/ui/component/livestreamLayout/view.jsx b/ui/component/livestreamLayout/view.jsx new file mode 100644 index 000000000..85c35cd0c --- /dev/null +++ b/ui/component/livestreamLayout/view.jsx @@ -0,0 +1,49 @@ +// @flow +import { BITWAVE_EMBED_URL } from 'constants/livestream'; +import React from 'react'; +import FileTitleSection from 'component/fileTitleSection'; +import LivestreamComments from 'component/livestreamComments'; + +type Props = { + uri: string, + claim: ?StreamClaim, + isLive: boolean, + activeViewers: number, +}; + +export default function LivestreamLayout(props: Props) { + const { claim, uri, isLive, activeViewers } = props; + + if (!claim || !claim.signing_channel) { + return null; + } + + const channelName = claim.signing_channel.name; + const channelClaimId = claim.signing_channel.claim_id; + + return ( + <> +
+
+
+