Compare commits
4 commits
master
...
feat/go-li
Author | SHA1 | Date | |
---|---|---|---|
|
af90085b26 | ||
|
1a8e7b0973 | ||
|
464f530264 | ||
|
a443caba71 |
8 changed files with 297 additions and 15 deletions
|
@ -1185,6 +1185,7 @@
|
|||
"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",
|
||||
"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",
|
||||
|
|
|
@ -100,6 +100,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
|
||||
|
@ -280,6 +281,7 @@ const Header = (props: Props) => {
|
|||
openSignOutModal={openSignOutModal}
|
||||
email={email}
|
||||
signOut={signOut}
|
||||
livestreamEnabled={livestreamEnabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -333,6 +335,7 @@ type HeaderMenuButtonProps = {
|
|||
openSignOutModal: () => void,
|
||||
email: ?string,
|
||||
signOut: () => void,
|
||||
livestreamEnabled: boolean,
|
||||
};
|
||||
|
||||
function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||
|
@ -346,6 +349,7 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
|||
openSignOutModal,
|
||||
email,
|
||||
signOut,
|
||||
livestreamEnabled,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
|
@ -374,6 +378,15 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
|||
<Icon aria-hidden icon={ICONS.CHANNEL} />
|
||||
{__('New Channel')}
|
||||
</MenuItem>
|
||||
|
||||
{/* Go Live Button for LiveStreaming */}
|
||||
{(livestreamEnabled) &&(
|
||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIVESTREAM}`)}>
|
||||
<Icon aria-hidden icon={ICONS.VIDEO} />
|
||||
{__('Go Live')}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
|
|
|
@ -17,9 +17,10 @@ import { doPublishDesktop } from 'redux/actions/publish';
|
|||
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
||||
import { selectModal, selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import PublishPage from './view';
|
||||
import PublishForm from './view';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = state => ({
|
||||
const select = (state) => ({
|
||||
...selectPublishFormValues(state),
|
||||
// The winning claim for a short lbry uri
|
||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||
|
@ -34,16 +35,17 @@ const select = state => ({
|
|||
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||
const perform = (dispatch) => ({
|
||||
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
|
||||
clearPublish: () => dispatch(doClearPublish()),
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
publish: (filePath, preview) => dispatch(doPublishDesktop(filePath, preview)),
|
||||
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||
checkAvailability: name => dispatch(doCheckPublishNameAvailability(name)),
|
||||
checkAvailability: (name) => dispatch(doCheckPublishNameAvailability(name)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(PublishPage);
|
||||
export default connect(select, perform)(PublishForm);
|
||||
|
|
|
@ -25,6 +25,7 @@ import SelectThumbnail from 'component/selectThumbnail';
|
|||
import Card from 'component/common/card';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import * as PUBLISH_MODES from 'constants/publish_types';
|
||||
import { FormField } from 'component/common/form';
|
||||
|
||||
// @if TARGET='app'
|
||||
import fs from 'fs';
|
||||
|
@ -72,14 +73,14 @@ type Props = {
|
|||
balance: number,
|
||||
isStillEditing: boolean,
|
||||
clearPublish: () => void,
|
||||
resolveUri: string => void,
|
||||
resolveUri: (string) => void,
|
||||
scrollToTop: () => void,
|
||||
prepareEdit: (claim: any, uri: string) => void,
|
||||
resetThumbnailStatus: () => void,
|
||||
amountNeededForTakeover: ?number,
|
||||
// Add back type
|
||||
updatePublishForm: any => void,
|
||||
checkAvailability: string => void,
|
||||
updatePublishForm: (any) => void,
|
||||
checkAvailability: (string) => void,
|
||||
ytSignupPending: boolean,
|
||||
modal: { id: string, modalProps: {} },
|
||||
enablePublishPreview: boolean,
|
||||
|
@ -124,8 +125,12 @@ function PublishForm(props: Props) {
|
|||
enablePublishPreview,
|
||||
activeChannelClaim,
|
||||
incognito,
|
||||
user,
|
||||
isLivestreamPublish,
|
||||
} = props;
|
||||
|
||||
const isLivestreamCreator = (user && user.experimental_ui) || false;
|
||||
|
||||
const TAGS_LIMIT = 5;
|
||||
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
|
||||
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
|
||||
|
@ -332,6 +337,38 @@ function PublishForm(props: Props) {
|
|||
<div className="card-stack">
|
||||
<ChannelSelect disabled={disabled} />
|
||||
|
||||
{/* Some LiveStream Publishing Hack Sean made */}
|
||||
{isLivestreamCreator && (
|
||||
<div className="livestream__creator-message livestream__publish-checkbox">
|
||||
<h4>Hello brave beta tester,</h4>
|
||||
<p>
|
||||
Check this box if you have entered video information for your livestream. It doesn't matter what file you
|
||||
choose for now, just make the sure the title, description, and tags are correct. Everything else is setup!
|
||||
</p>
|
||||
<p>
|
||||
When you edit this file, there will be another checkbox to turn this back into a regular video so it can be
|
||||
listed on your channel's page.
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
type={isStillEditing ? 'radio' : 'checkbox'}
|
||||
label={__('This is for my livestream')}
|
||||
name="is_livestream_checkbox"
|
||||
checked={isLivestreamPublish}
|
||||
onChange={(e) => updatePublishForm({ isLivestreamPublish: e.target.checked })}
|
||||
/>
|
||||
{isStillEditing && (
|
||||
<FormField
|
||||
type="radio"
|
||||
label={'I am done livestreaming'}
|
||||
name="is_livestream_checkbox_done"
|
||||
checked={!isLivestreamPublish}
|
||||
onChange={(e) => updatePublishForm({ isLivestreamPublish: !e.target.checked })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PublishFile
|
||||
uri={uri}
|
||||
mode={mode}
|
||||
|
@ -373,17 +410,17 @@ function PublishForm(props: Props) {
|
|||
"Add tags that are relevant to your content so those who're looking for it can find it more easily. If mature content, ensure it is tagged mature. Tag abuse and missing mature tags will not be tolerated."
|
||||
)}
|
||||
placeholder={__('gaming, crypto')}
|
||||
onSelect={newTags => {
|
||||
onSelect={(newTags) => {
|
||||
const validatedTags = [];
|
||||
newTags.forEach(newTag => {
|
||||
if (!tags.some(tag => tag.name === newTag.name)) {
|
||||
newTags.forEach((newTag) => {
|
||||
if (!tags.some((tag) => tag.name === newTag.name)) {
|
||||
validatedTags.push(newTag);
|
||||
}
|
||||
});
|
||||
updatePublishForm({ tags: [...tags, ...validatedTags] });
|
||||
}}
|
||||
onRemove={clickedTag => {
|
||||
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
|
||||
onRemove={(clickedTag) => {
|
||||
const newTags = tags.slice().filter((tag) => tag.name !== clickedTag.name);
|
||||
updatePublishForm({ tags: newTags });
|
||||
}}
|
||||
tagsChosen={tags}
|
||||
|
|
|
@ -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) {
|
|||
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.LIVESTREAM}`} component={LiveStreamPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.BUY}`} component={BuyPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
||||
|
|
|
@ -48,3 +48,4 @@ exports.BUY = 'buy';
|
|||
exports.CHANNEL_NEW = 'channel/new';
|
||||
exports.NOTIFICATIONS = 'notifications';
|
||||
exports.YOUTUBE_SYNC = 'youtube';
|
||||
exports.LIVESTREAM = 'livestream';
|
||||
|
|
13
ui/page/livestream/index.js
Normal file
13
ui/page/livestream/index.js
Normal file
|
@ -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);
|
213
ui/page/livestream/view.jsx
Normal file
213
ui/page/livestream/view.jsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
// @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<ChannelClaim>,
|
||||
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}?d=${toHex(activeChannelClaim.name)}&s=${sigData.signature}&t=${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 (
|
||||
<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 && activeChannelClaim && (
|
||||
<React.Fragment>
|
||||
{/* Channel Selector */}
|
||||
<ChannelSelector hideAnon />
|
||||
|
||||
{/* Display StreamKey */}
|
||||
{ streamKey
|
||||
? (<div>
|
||||
{/* Stream Server Address */}
|
||||
<FormField
|
||||
name={'livestreamServer'}
|
||||
label={'Stream Server'}
|
||||
type={'text'}
|
||||
defaultValue={'rtmp://stream.odysee.com/live'}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{/* Stream Key */}
|
||||
<FormField
|
||||
name={'livestreamKey'}
|
||||
label={'Stream Key'}
|
||||
type={'text'}
|
||||
defaultValue={streamKey}
|
||||
readOnly
|
||||
/>
|
||||
</div>)
|
||||
: (
|
||||
<div>
|
||||
<div style={{marginBottom: '2rem'}}>{JSON.stringify(activeChannelClaim)}</div>
|
||||
{ sigData &&
|
||||
<div>{JSON.stringify(sigData)}</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Stream Claim(s) */}
|
||||
{ livestreamClaim ? (
|
||||
<div style={{marginTop: 'var(--spacing-l)'}}>
|
||||
<h3>Your LiveStream Claims</h3>
|
||||
<ClaimPreview
|
||||
uri={livestreamClaim.permanent_url}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{marginTop: 'var(--spacing-l)'}}>
|
||||
<div>You must first publish a livestream claim before your stream will be visible!</div>
|
||||
<div>TODO: add a button for this</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debug Stuff */}
|
||||
{ streamKey && (
|
||||
<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>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue