From 25e999645caea0aa766745281ef5c2585da33caf Mon Sep 17 00:00:00 2001 From: DispatchCommit Date: Fri, 26 Feb 2021 08:12:32 -0800 Subject: [PATCH 01/54] Add Go Live to header dropdown --- static/app-strings.json | 1 + ui/component/header/view.jsx | 4 ++++ ui/constants/pages.js | 1 + 3 files changed, 6 insertions(+) diff --git a/static/app-strings.json b/static/app-strings.json index 9e5e45318..fa4c6acfe 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1172,6 +1172,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", diff --git a/ui/component/header/view.jsx b/ui/component/header/view.jsx index b02f7a8e1..cd3276ec2 100644 --- a/ui/component/header/view.jsx +++ b/ui/component/header/view.jsx @@ -422,6 +422,10 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) { {__('New Channel')} + history.push(`/$/${PAGES.GO_LIVE}`)}> + + {__('Go Live')} + )} diff --git a/ui/constants/pages.js b/ui/constants/pages.js index 3a55e58a3..e0a0c2c4a 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -48,3 +48,4 @@ exports.BUY = 'buy'; exports.CHANNEL_NEW = 'channel/new'; exports.NOTIFICATIONS = 'notifications'; exports.YOUTUBE_SYNC = 'youtube'; +exports.GO_LIVE = 'livestream'; -- 2.45.3 From dc1738f1a58b4201cb1d9656fc1389046876c759 Mon Sep 17 00:00:00 2001 From: DispatchCommit Date: Mon, 1 Mar 2021 18:07:10 -0800 Subject: [PATCH 02/54] Create livestream page and generate signed streamkey --- ui/component/header/view.jsx | 37 ++++++-- ui/component/router/view.jsx | 2 + ui/constants/pages.js | 2 +- ui/page/livestream/index.js | 13 +++ ui/page/livestream/view.jsx | 163 +++++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 ui/page/livestream/index.js create mode 100644 ui/page/livestream/view.jsx diff --git a/ui/component/header/view.jsx b/ui/component/header/view.jsx index cd3276ec2..3644c34b6 100644 --- a/ui/component/header/view.jsx +++ b/ui/component/header/view.jsx @@ -99,6 +99,7 @@ const Header = (props: Props) => { const hasBackout = Boolean(backout); const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {}; const notificationsEnabled = (user && user.experimental_ui) || false; + const livestreamEnabled = (user && user.experimental_ui) || false; const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url; // Sign out if they click the "x" when they are on the password prompt @@ -275,6 +276,11 @@ const Header = (props: Props) => { history={history} handleThemeToggle={handleThemeToggle} currentTheme={currentTheme} + activeChannelUrl={activeChannelUrl} + openSignOutModal={openSignOutModal} + email={email} + signOut={signOut} + livestreamEnabled={livestreamEnabled} /> )} @@ -391,10 +397,26 @@ type HeaderMenuButtonProps = { history: { push: (string) => void }, handleThemeToggle: (string) => void, currentTheme: string, + activeChannelUrl: ?string, + openSignOutModal: () => void, + email: ?string, + signOut: () => void, + livestreamEnabled: boolean, }; function HeaderMenuButtons(props: HeaderMenuButtonProps) { - const { authenticated, notificationsEnabled, history, handleThemeToggle, currentTheme } = props; + const { + authenticated, + notificationsEnabled, + history, + handleThemeToggle, + currentTheme, + activeChannelUrl, + openSignOutModal, + email, + signOut, + livestreamEnabled, + } = props; return (
@@ -422,10 +444,15 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) { {__('New Channel')} - history.push(`/$/${PAGES.GO_LIVE}`)}> - - {__('Go Live')} - + + {/* Go Live Button for LiveStreaming */} + {(livestreamEnabled) &&( + history.push(`/$/${PAGES.LIVESTREAM}`)}> + + {__('Go Live')} + + )} + )} diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index 63918e4d5..738e81a9d 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -36,6 +36,7 @@ import PasswordResetPage from 'page/passwordReset'; import PasswordSetPage from 'page/passwordSet'; import SignInVerifyPage from 'page/signInVerify'; import ChannelsPage from 'page/channels'; +import LiveStreamPage from 'page/livestream'; import EmbedWrapperPage from 'page/embedWrapper'; import TopPage from 'page/top'; import Welcome from 'page/welcome'; @@ -275,6 +276,7 @@ function AppRouter(props: Props) { + diff --git a/ui/constants/pages.js b/ui/constants/pages.js index e0a0c2c4a..b2bc1f0ba 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -48,4 +48,4 @@ exports.BUY = 'buy'; exports.CHANNEL_NEW = 'channel/new'; exports.NOTIFICATIONS = 'notifications'; exports.YOUTUBE_SYNC = 'youtube'; -exports.GO_LIVE = 'livestream'; +exports.LIVESTREAM = 'livestream'; diff --git a/ui/page/livestream/index.js b/ui/page/livestream/index.js new file mode 100644 index 000000000..9c724308c --- /dev/null +++ b/ui/page/livestream/index.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux'; +import { selectActiveChannelClaim } from 'redux/selectors/app'; +import { doSetActiveChannel } from 'redux/actions/app'; +import CreatorDashboardPage from './view'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), + activeChannelClaim: selectActiveChannelClaim(state), +}); + +export default connect(select, { doSetActiveChannel })(CreatorDashboardPage); diff --git a/ui/page/livestream/view.jsx b/ui/page/livestream/view.jsx new file mode 100644 index 000000000..a956cb740 --- /dev/null +++ b/ui/page/livestream/view.jsx @@ -0,0 +1,163 @@ +// @flow +import * as PAGES from 'constants/pages'; +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-redux'; +import { toHex } from '../../util/hex'; +import ClaimPreview from '../../component/claimPreview'; +import { FormField } from '../../component/common/form'; + +type Props = { + channels: Array, + fetchingChannels: boolean, + activeChannelClaim: ?ChannelClaim, +}; + +export default function CreatorDashboardPage(props: Props) { + const { channels, fetchingChannels, activeChannelClaim } = props; + + const [sigData, setSigData] = React.useState({ signature: undefined, signing_ts: undefined }); + + const hasChannels = channels && channels.length > 0; + const activeChannelClaimStr = JSON.stringify(activeChannelClaim); + const streamKey = createStreamKey(); + + React.useEffect(() => { + if (activeChannelClaimStr) { + const channelClaim = JSON.parse(activeChannelClaimStr); + + // ensure we have a channel + if (channelClaim.claim_id) { + Lbry.channel_sign({ + channel_id: channelClaim.claim_id, + hexdata: toHex(channelClaim.name), + }) + .then((data) => { + console.log(data); + setSigData(data); + }) + .catch((error) => { + setSigData({ signature: null, signing_ts: null }); + console.error(error); + }); + } + } + }, [ activeChannelClaimStr, setSigData ]); + + function createStreamKey() { + if (!activeChannelClaim || !sigData.signature || !sigData.signing_ts) return null; + return `${activeChannelClaim.claim_id}?sig=${sigData.signature}&ts=${sigData.signing_ts}`; + } + + /******/ + + const LIVE_STREAM_TAG = 'odysee-livestream'; + + const [isFetching, setIsFetching] = React.useState(true); + const [isLive, setIsLive] = React.useState(false); + const [livestreamClaim, setLivestreamClaim] = React.useState(false); + + React.useEffect(() => { + if (!activeChannelClaimStr) return; + + const channelClaim = JSON.parse(activeChannelClaimStr); + + Lbry.claim_search({ + channel_ids: [channelClaim.claim_id], + any_tags: [LIVE_STREAM_TAG], + claim_type: ['stream'], + }) + .then((res) => { + if (res && res.items && res.items.length > 0) { + const claim = res.items[0]; + setLivestreamClaim(claim); + } else { + setIsFetching(false); + } + }) + .catch(() => { + setIsFetching(false); + }); + }, [activeChannelClaimStr]); + + return ( + + {fetchingChannels && ( +
+ +
+ )} + + {!fetchingChannels && !hasChannels && ( + +
+ } + /> + )} + + {!fetchingChannels && activeChannelClaim && ( + + {/* Channel Selector */} + + + {/* Display StreamKey */} + { streamKey + ? (
+ {/* Stream Server Address */} + + + {/* Stream Key */} + +
) + : ( +
+
{JSON.stringify(activeChannelClaim)}
+ { sigData && +
{JSON.stringify(sigData)}
+ } +
+ ) + } + + {/* Stream Claim(s) */} + { livestreamClaim ? ( +
+

Your LiveStream Claims

+ +
+ ) : ( +
+
You must first publish a livestream claim before your stream will be visible!
+
TODO: add a button for this
+
+ )} + + {activeChannelClaim && +
Public Key: {activeChannelClaim.value.public_key}
+ } +
+ )} + + ); +} -- 2.45.3 From 5e1240df42529d88183f7a3c61d4e73a6e3b386a Mon Sep 17 00:00:00 2001 From: DispatchCommit Date: Wed, 3 Mar 2021 18:55:08 -0800 Subject: [PATCH 03/54] Add channel name hex data to streamkey Also adds individual debug fields to help when debugging a channel verify currently --- ui/page/livestream/view.jsx | 62 +++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/ui/page/livestream/view.jsx b/ui/page/livestream/view.jsx index a956cb740..192d291b0 100644 --- a/ui/page/livestream/view.jsx +++ b/ui/page/livestream/view.jsx @@ -50,7 +50,7 @@ export default function CreatorDashboardPage(props: Props) { function createStreamKey() { if (!activeChannelClaim || !sigData.signature || !sigData.signing_ts) return null; - return `${activeChannelClaim.claim_id}?sig=${sigData.signature}&ts=${sigData.signing_ts}`; + return `${activeChannelClaim.claim_id}?d=${toHex(activeChannelClaim.name)}&s=${sigData.signature}&t=${sigData.signing_ts}`; } /******/ @@ -143,8 +143,10 @@ export default function CreatorDashboardPage(props: Props) { {/* Stream Claim(s) */} { livestreamClaim ? (
-

Your LiveStream Claims

- +

Your LiveStream Claims

+
) : (
@@ -153,9 +155,57 @@ export default function CreatorDashboardPage(props: Props) {
)} - {activeChannelClaim && -
Public Key: {activeChannelClaim.value.public_key}
- } + {/* Debug Stuff */} + { streamKey && ( +
+

Debug Info

+ + {/* Channel ID */} + + + {/* Signature */} + + + {/* Signature TS */} + + + {/* Hex Data */} + + + {/* Channel Public Key */} + +
+ )} )} -- 2.45.3 From 7dc44194f95247fda4ddd6e39fda4fabe2d6923a Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Wed, 10 Mar 2021 13:34:21 -0500 Subject: [PATCH 04/54] bring in livestream changes from odysee --- package.json | 1 + ui/component/commentCreate/index.js | 12 +- ui/component/commentCreate/view.jsx | 47 +++++- ui/component/livestreamComments/index.js | 14 ++ ui/component/livestreamComments/view.jsx | 129 +++++++++++++++ ui/component/livestreamLayout/index.js | 9 ++ ui/component/livestreamLayout/view.jsx | 34 ++++ ui/component/livestreamLink/index.js | 9 ++ ui/component/livestreamLink/view.jsx | 68 ++++++++ ui/component/page/view.jsx | 5 +- ui/constants/action_types.js | 1 + ui/constants/livestream.js | 4 + ui/page/livestream/index.js | 2 +- ui/page/livestreamStream/index.js | 18 +++ ui/page/livestreamStream/view.jsx | 114 +++++++++++++ ui/page/show/index.js | 8 +- ui/page/show/view.jsx | 7 + ui/redux/actions/app.js | 44 ++--- ui/redux/actions/websocket.js | 61 ++++--- ui/scss/all.scss | 1 + ui/scss/component/_button.scss | 5 + ui/scss/component/_livestream.scss | 198 +++++++++++++++++++++++ ui/scss/component/_main.scss | 4 + yarn.lock | 59 +++++++ 24 files changed, 804 insertions(+), 50 deletions(-) create mode 100644 ui/component/livestreamComments/index.js create mode 100644 ui/component/livestreamComments/view.jsx create mode 100644 ui/component/livestreamLayout/index.js create mode 100644 ui/component/livestreamLayout/view.jsx create mode 100644 ui/component/livestreamLink/index.js create mode 100644 ui/component/livestreamLink/view.jsx create mode 100644 ui/constants/livestream.js create mode 100644 ui/page/livestreamStream/index.js create mode 100644 ui/page/livestreamStream/view.jsx create mode 100644 ui/scss/component/_livestream.scss diff --git a/package.json b/package.json index 0b5d72490..7af6adf43 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/ui/component/commentCreate/index.js b/ui/component/commentCreate/index.js index 6644cae2a..2f69ff259 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,14 @@ 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)), 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..814b7f71e 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,7 +96,18 @@ export function CommentCreate(props: Props) { function handleSubmit() { if (activeChannelClaim && commentValue.length) { - createComment(commentValue, claimId, parentId).then(res => { + const timeUntilCanComment = !lastCommentTime + ? 0 + : lastCommentTime / 1000 - 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.floor(timeUntilCanComment) }) + ); + return; + } + + createComment(commentValue, claimId, parentId).then((res) => { if (res && res.signature) { setCommentValue(''); @@ -144,6 +172,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..eee30cee8 --- /dev/null +++ b/ui/component/livestreamLayout/index.js @@ -0,0 +1,9 @@ +import { connect } from 'react-redux'; +import { makeSelectClaimForUri } from 'lbry-redux'; +import LivestreamLayout from './view'; + +const select = (state, props) => ({ + claim: makeSelectClaimForUri(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..8ac8e6dbf --- /dev/null +++ b/ui/component/livestreamLayout/view.jsx @@ -0,0 +1,34 @@ +// @flow +import { BITWAVE_EMBED_URL } from 'constants/livestream'; +import React from 'react'; +import FileTitle from 'component/fileTitle'; +import LivestreamComments from 'component/livestreamComments'; + +type Props = { + uri: string, + claim: ?StreamClaim, + activeViewers: number, +}; + +export default function LivestreamLayout(props: Props) { + const { claim, uri, activeViewers } = props; + + if (!claim) { + return null; + } + + return ( + <> +
+
+
+