tom rebase attempt #5881

Closed
tzarebczan wants to merge 10 commits from replays-rebased-tom into master
13 changed files with 599 additions and 120 deletions
Showing only changes of commit 777f406abe - Show all commits

22
flow-typed/livestream.js vendored Normal file
View file

@ -0,0 +1,22 @@
// @flow
declare type LivestreamReplayItem = {
data: {
claimId: string,
deleted: boolean,
deletedAt: ?string,
ffprobe: any,
fileDuration: number, // decimal? float? string?
fileType: string,
fileLocation: string,
fileSize: number,
key: string,
published: boolean,
publishedAt: ?string,
service: string,
thumbnails: Array<string>,
uploadedAt: string, // Date?
},
id: string,
}
declare type LivestreamReplayData = Array<LivestreamReplayItem>;

View file

@ -1,15 +1,4 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import {
DOWNLOADED,
DOWNLOADING,
NOTIFY_ONLY,
VIEW_ALL,
VIEW_LATEST_FIRST,
SUGGESTED_TOP_BID,
SUGGESTED_TOP_SUBSCRIBED,
SUGGESTED_FEATURED,
} from 'constants/subscriptions';
declare type Subscription = {
channelName: string, // @CryptoCandor,

View file

@ -1777,6 +1777,11 @@
"Learn more and sign petition": "Learn more and sign petition",
"Publishing...": "Publishing...",
"Collection": "Collection",
"Upload that unlabeled video you found behind the TV in 1991": "Upload that unlabeled video you found behind the TV in 1991",
"Select Replay": "Select Replay",
"Craft an epic post clearly explaining... whatever.": "Craft an epic post clearly explaining... whatever.",
"waiting": "waiting",
"%viewer_count% currently %viewer_state%": "%viewer_count% currently %viewer_state%",
"More from %claim_name%": "More from %claim_name%",
"--end--": "--end--"
}

View file

@ -3,7 +3,6 @@ import { SIMPLE_SITE, SITE_NAME, ENABLE_FILE_REACTIONS } from 'config';
import * as PAGES from 'constants/pages';
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons';
import * as PUBLISH_MODES from 'constants/publish_types';
import React from 'react';
import Button from 'component/button';
import FileDownloadLink from 'component/fileDownloadLink';
@ -114,25 +113,13 @@ function FileActions(props: Props) {
<Button
className="button--file-action"
icon={ICONS.EDIT}
label={__('Edit')}
navigate={`/$/${PAGES.UPLOAD}${isLivestreamClaim ? `?type=${PUBLISH_MODES.LIVESTREAM}` : ''}`}
label={isLivestreamClaim ? __('Update') : __('Edit')}
navigate={`/$/${PAGES.UPLOAD}`}
onClick={() => {
prepareEdit(claim, editUri, fileInfo);
}}
/>
)}
{claimIsMine && isLivestreamClaim && (
<Button
className="button--file-action"
icon={ICONS.PUBLISH}
label={__('Publish Replay')}
navigate={`/$/${PAGES.UPLOAD}?type=${PUBLISH_MODES.FILE}`}
onClick={() => {
prepareEdit(claim, editUri, fileInfo);
}}
/>
)}
{showDelete && (
<Button
title={__('Remove from your library')}

View file

@ -5,15 +5,17 @@ import {
makeSelectPublishFormValue,
doUpdatePublishForm,
doClearPublish,
makeSelectClaimIsStreamPlaceholder,
} from 'lbry-redux';
import { doToast } from 'redux/actions/notifications';
import { selectFfmpegStatus } from 'redux/selectors/settings';
import PublishPage from './view';
const select = state => ({
const select = (state, props) => ({
name: makeSelectPublishFormValue('name')(state),
title: makeSelectPublishFormValue('title')(state),
filePath: makeSelectPublishFormValue('filePath')(state),
remoteUrl: makeSelectPublishFormValue('remoteFileUrl')(state),
optimize: makeSelectPublishFormValue('optimize')(state),
isStillEditing: selectIsStillEditing(state),
balance: selectBalance(state),
@ -22,12 +24,13 @@ const select = state => ({
size: makeSelectPublishFormValue('fileSize')(state),
duration: makeSelectPublishFormValue('fileDur')(state),
isVid: makeSelectPublishFormValue('fileVid')(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
});
const perform = dispatch => ({
const perform = (dispatch) => ({
clearPublish: () => dispatch(doClearPublish()),
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
showToast: message => dispatch(doToast({ message, isError: true })),
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),
showToast: (message) => dispatch(doToast({ message, isError: true })),
});
export default connect(select, perform)(PublishPage);

View file

@ -8,12 +8,16 @@ import PostEditor from 'component/postEditor';
import FileSelector from 'component/common/file-selector';
import Button from 'component/button';
import Card from 'component/common/card';
import { FormField } from 'component/common/form';
import { Form, FormField } from 'component/common/form';
import Spinner from 'component/spinner';
import I18nMessage from 'component/i18nMessage';
import usePersistedState from 'effects/use-persisted-state';
import * as PUBLISH_MODES from 'constants/publish_types';
import PublishName from 'component/publishName';
import CopyableText from 'component/copyableText';
import moment from 'moment';
import classnames from 'classnames';
import ReactPaginate from 'react-paginate';
type Props = {
uri: ?string,
@ -35,9 +39,15 @@ type Props = {
size: number,
duration: number,
isVid: boolean,
subtitle: string,
setPublishMode: (string) => void,
setPrevFileText: (string) => void,
header: Node,
livestreamData: LivestreamReplayData,
isLivestreamClaim: boolean,
remoteUrl?: string,
checkLivestreams: () => void,
isCheckingLivestreams: boolean,
};
function PublishFile(props: Props) {
@ -63,29 +73,58 @@ function PublishFile(props: Props) {
setPublishMode,
setPrevFileText,
header,
livestreamData,
isLivestreamClaim,
subtitle,
remoteUrl,
checkLivestreams,
isCheckingLivestreams,
} = props;
const SOURCE_NONE = 'none';
const SOURCE_SELECT = 'select';
const SOURCE_UPLOAD = 'upload';
const RECOMMENDED_BITRATE = 6000000;
const TV_PUBLISH_SIZE_LIMIT: number = 4294967296;
const TV_PUBLISH_SIZE_LIMIT_STR_GB = '4';
const PAGE_SIZE = 4;
const PROCESSING_MB_PER_SECOND = 0.5;
const MINUTES_THRESHOLD = 30;
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
const MARKDOWN_FILE_EXTENSIONS = ['txt', 'md', 'markdown'];
const sizeInMB = Number(size) / 1000000;
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
const fileSelectorModes = [
{ label: __('Select Replay'), actionName: SOURCE_SELECT, icon: ICONS.MENU },
{ label: __('Upload'), actionName: SOURCE_UPLOAD, icon: ICONS.PUBLISH },
{ label: __('Not Yet'), actionName: SOURCE_NONE, icon: ICONS.REMOVE },
];
const hasLivestreamData = livestreamData && Boolean(livestreamData.length);
const showSourceSelector = isLivestreamClaim && hasLivestreamData;
const ffmpegAvail = ffmpegStatus.available;
const [oversized, setOversized] = useState(false);
const [currentFile, setCurrentFile] = useState(null);
const [currentFileType, setCurrentFileType] = useState(null);
const [optimizeAvail, setOptimizeAvail] = useState(false);
const [userOptimize, setUserOptimize] = usePersistedState('publish-file-user-optimize', false);
const [fileSelectSource, setFileSelectSource] = useState(
IS_WEB && showSourceSelector ? SOURCE_SELECT : SOURCE_UPLOAD
);
// const [showFileUpdate, setShowFileUpdate] = useState(false);
const [selectedFileIndex, setSelectedFileIndex] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const totalPages =
hasLivestreamData && livestreamData.length > PAGE_SIZE ? Math.ceil(livestreamData.length / PAGE_SIZE) : 1;
const RECOMMENDED_BITRATE = 6000000;
const TV_PUBLISH_SIZE_LIMIT: number = 4294967296;
const TV_PUBLISH_SIZE_LIMIT_STR_GB = '4';
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_STR_GB }
);
const PROCESSING_MB_PER_SECOND = 0.5;
const MINUTES_THRESHOLD = 30;
const HOURS_THRESHOLD = MINUTES_THRESHOLD * 60;
const MARKDOWN_FILE_EXTENSIONS = ['txt', 'md', 'markdown'];
const sizeInMB = Number(size) / 1000000;
const secondsToProcess = sizeInMB / PROCESSING_MB_PER_SECOND;
// Reset filePath if publish mode changed
useEffect(() => {
@ -93,9 +132,40 @@ function PublishFile(props: Props) {
if (currentFileType !== 'text/markdown' && !isStillEditing) {
updatePublishForm({ filePath: '' });
}
} else if (mode === PUBLISH_MODES.LIVESTREAM) {
updatePublishForm({ filePath: '' });
}
}, [currentFileType, mode, isStillEditing, updatePublishForm]);
// set default file source to select if necessary
useEffect(() => {
if (hasLivestreamData && isLivestreamClaim) {
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(() => {
if (selectedFileIndex !== null) {
updatePublishForm({
remoteFileUrl: normalizeUrlForProtocol(livestreamData[selectedFileIndex].data.fileLocation),
});
}
}, [selectedFileIndex, updatePublishForm]);
useEffect(() => {
if (!filePath || filePath === '') {
setCurrentFile('');
@ -121,6 +191,10 @@ function PublishFile(props: Props) {
updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid });
}
function handlePaginateReplays(page) {
setCurrentPage(page);
}
function getBitrate(size, duration) {
const s = Number(size);
const d = Number(duration);
@ -154,7 +228,14 @@ function PublishFile(props: Props) {
}
}
function getMessage() {
function getSourceMessage() {
if (remoteUrl || filePath) {
return __('Replay selected for update');
} else {
return __('No replay selected');
}
}
function getUploadMessage() {
// @if TARGET='web'
if (oversized) {
return (
@ -186,6 +267,11 @@ function PublishFile(props: Props) {
}
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 (
<p className="help">
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
@ -225,6 +311,26 @@ function PublishFile(props: Props) {
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);
updatePublishForm({ remoteFileUrl: undefined });
} else if (source === SOURCE_UPLOAD) {
updatePublishForm({ remoteFileUrl: undefined });
} else if (source === SOURCE_SELECT) {
// $FlowFixMe
handleFileChange(false);
if (selectedFileIndex !== null) {
updatePublishForm({ remoteFileUrl: livestreamData[selectedFileIndex].data.fileLocation });
}
}
setFileSelectSource(source);
}
function handleTitleChange(event) {
const title = event.target.value;
// Update title
@ -247,7 +353,11 @@ function PublishFile(props: Props) {
// select file, start to select a new one, then cancel
if (!file) {
updatePublishForm({ filePath: '', name: '' });
if (isStillEditing) {
updatePublishForm({ filePath: '' });
} else {
updatePublishForm({ filePath: '', name: '' });
}
return;
}
@ -326,7 +436,7 @@ function PublishFile(props: Props) {
updatePublishForm(publishFormParams);
}
const isPublishFile = mode === PUBLISH_MODES.FILE;
const showFileUpload = mode === PUBLISH_MODES.FILE || (mode === PUBLISH_MODES.LIVESTREAM && hasLivestreamData);
const isPublishPost = mode === PUBLISH_MODES.POST;
return (
@ -334,7 +444,7 @@ function PublishFile(props: Props) {
className={disabled || balance === 0 ? 'card--disabled' : ''}
title={
<div>
{header}
{header} {/* display mode buttons from parent */}
{publishing && <Spinner type={'small'} />}
{inProgress && (
<div>
@ -343,10 +453,10 @@ function PublishFile(props: Props) {
)}
</div>
}
subtitle={isStillEditing && __('You are currently editing your upload.')}
subtitle={subtitle || (isStillEditing && __('You are currently editing your upload.'))}
actions={
<React.Fragment>
<PublishName />
<PublishName uri={uri} />
<FormField
type="text"
name="content_title"
@ -356,28 +466,154 @@ function PublishFile(props: Props) {
value={title}
onChange={handleTitleChange}
/>
{isPublishFile && (
{/* Decide whether to show file upload or replay selector */}
{/* @if TARGET='web' */}
<>
{showSourceSelector && (
<fieldset-section>
<div className="section__actions--between">
<div>
<label>{__('Add replay video')}</label>
<div>
{fileSelectorModes.map((fmode) => (
<Button
key={fmode.label}
icon={fmode.icon}
iconSize={18}
label={fmode.label}
button="alt"
onClick={() => {
// $FlowFixMe
handleFileSource(fmode.actionName);
}}
className={classnames('button-toggle', {
'button-toggle--active': fileSelectSource === fmode.actionName,
})}
/>
))}
</div>
<div className="help--inline">{getSourceMessage()}</div>
</div>
<Button
button="secondary"
label={__('Check for Replays')}
disabled={isCheckingLivestreams}
icon={ICONS.REFRESH}
onClick={() => checkLivestreams()}
/>
</div>
</fieldset-section>
)}
{fileSelectSource === SOURCE_UPLOAD && showFileUpload && (
<>
<FileSelector
label={__('Video file')}
disabled={disabled}
currentPath={currentFile}
onFileChosen={handleFileChange}
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
accept="video/mp4,video/x-m4v,video/*"
placeholder={__('Select video file to upload')}
/>
{getUploadMessage()}
</>
)}
{fileSelectSource === SOURCE_SELECT && showFileUpload && (
<>
<fieldset-section>
<label>{__('Select Replay')}</label>
<div className="table__wrapper">
<table className="table table--livestream-data">
<tbody>
{livestreamData &&
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>
<Form style={totalPages <= 1 ? { display: 'none' } : null} onSubmit={handlePaginateReplays}>
<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>
</Form>
</>
)}
</>
{/* @endif */}
{/* @if TARGET='app' */}
{showFileUpload && (
<FileSelector
label={__('File')}
label={__('Video file')}
disabled={disabled}
currentPath={currentFile}
onFileChosen={handleFileChange}
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
accept="video/mp4,video/x-m4v,video/*"
placeholder={__('Select video file to upload')}
/>
)}
{isPublishPost && (
<PostEditor
label={__('Post --[noun, markdown post tab button]--')}
uri={uri}
disabled={disabled}
fileMimeType={fileMimeType}
setPrevFileText={setPrevFileText}
setCurrentFileType={setCurrentFileType}
/>
)}
{isPublishFile && getMessage()}
{/* @if TARGET='app' */}
{isPublishFile && (
{showFileUpload && (
<FormField
type="checkbox"
checked={userOptimize}
@ -387,7 +623,7 @@ function PublishFile(props: Props) {
name="optimize"
/>
)}
{isPublishFile && !ffmpegAvail && (
{showFileUpload && !ffmpegAvail && (
<p className="help">
<I18nMessage
tokens={{
@ -398,7 +634,7 @@ function PublishFile(props: Props) {
</I18nMessage>
</p>
)}
{isPublishFile && Boolean(size) && ffmpegAvail && optimize && isVid && (
{showFileUpload && Boolean(size) && ffmpegAvail && optimize && isVid && (
<p className="help">
<I18nMessage
tokens={{
@ -412,6 +648,16 @@ function PublishFile(props: Props) {
</p>
)}
{/* @endif */}
{isPublishPost && (
<PostEditor
label={__('Post --[noun, markdown post tab button]--')}
uri={uri}
disabled={disabled}
fileMimeType={fileMimeType}
setPrevFileText={setPrevFileText}
setCurrentFileType={setCurrentFileType}
/>
)}
</React.Fragment>
}
/>

View file

@ -12,7 +12,10 @@ import {
doPrepareEdit,
doCheckPublishNameAvailability,
SETTINGS,
selectMyChannelClaims,
makeSelectClaimIsStreamPlaceholder,
} from 'lbry-redux';
import * as RENDER_MODES from 'constants/file_render_modes';
import { doPublishDesktop } from 'redux/actions/publish';
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
import {
@ -22,27 +25,36 @@ import {
selectActiveChannelStakedLevel,
} from 'redux/selectors/app';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import PublishPage from './view';
import { selectUser } from 'redux/selectors/user';
const select = (state) => ({
...selectPublishFormValues(state),
user: selectUser(state),
// The winning claim for a short lbry uri
amountNeededForTakeover: selectTakeOverAmount(state),
// My previously published claims under this short lbry uri
myClaimForUri: selectMyClaimForUri(state),
// If I clicked the "edit" button, have I changed the uri?
// Need this to make it easier to find the source on previously published content
isStillEditing: selectIsStillEditing(state),
isResolvingUri: selectIsResolvingPublishUris(state),
totalRewardValue: selectUnclaimedRewardValue(state),
modal: selectModal(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
});
const select = (state) => {
const myClaimForUri = selectMyClaimForUri(state);
const permanentUrl = (myClaimForUri && myClaimForUri.permanent_url) || '';
const isPostClaim = makeSelectFileRenderModeForUri(permanentUrl)(state) === RENDER_MODES.MARKDOWN;
return {
...selectPublishFormValues(state),
user: selectUser(state),
// The winning claim for a short lbry uri
amountNeededForTakeover: selectTakeOverAmount(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(permanentUrl)(state),
isPostClaim,
permanentUrl,
// My previously published claims under this short lbry uri
// If I clicked the "edit" button, have I changed the uri?
// Need this to make it easier to find the source on previously published content
myClaimForUri,
isStillEditing: selectIsStillEditing(state),
isResolvingUri: selectIsResolvingPublishUris(state),
totalRewardValue: selectUnclaimedRewardValue(state),
modal: selectModal(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
activeChannelClaim: selectActiveChannelClaim(state),
myChannels: selectMyChannelClaims(state),
incognito: selectIncognito(state),
};
};
const perform = (dispatch) => ({
updatePublishForm: (value) => dispatch(doUpdatePublishForm(value)),

View file

@ -10,7 +10,7 @@
import { SITE_NAME, ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
import React, { useEffect } from 'react';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES, Lbry } from 'lbry-redux';
import Button from 'component/button';
import ChannelSelect from 'component/channelSelector';
import classnames from 'classnames';
@ -27,6 +27,7 @@ import I18nMessage from 'component/i18nMessage';
import * as PUBLISH_MODES from 'constants/publish_types';
import { useHistory } from 'react-router';
import Spinner from 'component/spinner';
import { toHex } from 'util/hex';
// @if TARGET='app'
import fs from 'fs';
@ -82,6 +83,9 @@ type Props = {
incognito: boolean,
user: ?User,
activeChannelStakedLevel: number,
isLivestreamClaim: boolean,
isPostClaim: boolean,
permanentUrl: ?string,
};
function PublishForm(props: Props) {
@ -114,28 +118,67 @@ function PublishForm(props: Props) {
incognito,
user,
activeChannelStakedLevel,
} = props;
isLivestreamClaim,
isPostClaim,
permanentUrl,
const { replace, location } = useHistory();
const urlParams = new URLSearchParams(location.search);
const uploadType = urlParams.get('type');
const livestreamEnabled =
ENABLE_NO_SOURCE_CLAIMS &&
const TYPE_PARAM = 'type';
const uploadType = urlParams.get(TYPE_PARAM);
const enableLivestream = ENABLE_NO_SOURCE_CLAIMS &&
user &&
!user.odysee_live_disabled &&
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
// $FlowFixMe
const MODES = livestreamEnabled
? Object.values(PUBLISH_MODES)
: Object.values(PUBLISH_MODES).filter((mode) => mode !== PUBLISH_MODES.LIVESTREAM);
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
if (editingURI) {
if (isPostClaim) {
return mode === PUBLISH_MODES.POST;
} else if (isLivestreamClaim) {
return mode === PUBLISH_MODES.LIVESTREAM && enableLivestream;
} else {
return mode === PUBLISH_MODES.FILE;
}
} else {
if (mode === PUBLISH_MODES.LIVESTREAM) {
return enableLivestream;
} else {
return true;
}
}
});
const MODE_TO_I18N_STR = {
[PUBLISH_MODES.FILE]: SIMPLE_SITE ? 'Video' : 'File',
[PUBLISH_MODES.POST]: 'Post --[noun, markdown post tab button]--',
[PUBLISH_MODES.LIVESTREAM]: 'Livestream --[noun, livestream tab button]--',
};
// Component state
const [mode, setMode] = React.useState(uploadType || PUBLISH_MODES.FILE);
const [isCheckingLivestreams, setCheckingLivestreams] = React.useState(false);
let customSubtitle;
if (mode === PUBLISH_MODES.LIVESTREAM || isLivestreamClaim) {
if (isLivestreamClaim) {
customSubtitle = __('Update your livestream');
} else {
customSubtitle = __('Prepare an upcoming livestream');
}
} else if (mode === PUBLISH_MODES.POST || isPostClaim) {
if (isPostClaim) {
customSubtitle = __('Edit your post');
} else {
customSubtitle = __('Craft an epic post clearly explaining... whatever.');
}
} else {
if (editingURI) {
customSubtitle = __('Update your video');
} else {
customSubtitle = __('Upload that unlabeled video you found behind the TV in 1991');
}
}
const [autoSwitchMode, setAutoSwitchMode] = React.useState(true);
// Used to check if the url name has changed:
@ -145,18 +188,23 @@ function PublishForm(props: Props) {
const [fileEdited, setFileEdited] = React.useState(false);
const [prevFileText, setPrevFileText] = React.useState('');
const [livestreamData, setLivestreamData] = React.useState([]);
const [signedMessage, setSignedMessage] = React.useState({});
const signedMessageStr = JSON.stringify(signedMessage);
const TAGS_LIMIT = 5;
const fileFormDisabled = mode === PUBLISH_MODES.FILE && !filePath;
const emptyPostError = mode === PUBLISH_MODES.POST && (!fileText || fileText.trim() === '');
const formDisabled = (fileFormDisabled && !editingURI) || emptyPostError || publishing;
const isInProgress = filePath || editingURI || name || title;
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
const activeChannelClaimStr = JSON.stringify(activeChannelClaim);
// Editing content info
const uri = myClaimForUri ? myClaimForUri.permanent_url : undefined;
const fileMimeType =
myClaimForUri && myClaimForUri.value && myClaimForUri.value.source
? myClaimForUri.value.source.media_type
: undefined;
const claimChannelId = myClaimForUri && myClaimForUri.signing_channel && myClaimForUri.signing_channel.claim_id;
const nameEdited = isStillEditing && name !== prevName;
// If they are editing, they don't need a new file chosen
@ -178,6 +226,29 @@ function PublishForm(props: Props) {
: formValidLessFile;
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) => {
console.log('data', data);
setSignedMessage(data);
})
.catch((error) => {
setSignedMessage({ signature: null, signing_ts: null });
});
}
}
}, [activeChannelClaimStr, setSignedMessage]);
useEffect(() => {
if (!modal) {
setTimeout(() => {
@ -186,12 +257,38 @@ function PublishForm(props: Props) {
}
}, [modal]);
const isLivestream = mode === PUBLISH_MODES.LIVESTREAM;
// move this to lbryinc OR to a file under ui, and/or provide a standardized livestreaming config.
function checkLivestreams(channelId, signature, timestamp) {
// $FlowFixMe Bitwave's API can handle garbage
setCheckingLivestreams(true);
fetch(`https://api.bitwave.tv/v1/replays/odysee/${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 && isLivestreamClaim && signedMessage.signature) {
checkLivestreams(claimChannelId, signedMessage.signature, signedMessage.signing_ts);
}
}, [claimChannelId, isLivestreamClaim, signedMessageStr]);
const isLivestreamMode = mode === PUBLISH_MODES.LIVESTREAM;
let submitLabel;
if (publishing) {
if (isStillEditing) {
submitLabel = __('Saving...');
} else if (isLivestream) {
} else if (isLivestreamMode) {
submitLabel = __('Creating...');
} else {
submitLabel = __('Uploading...');
@ -201,7 +298,7 @@ function PublishForm(props: Props) {
} else {
if (isStillEditing) {
submitLabel = __('Save');
} else if (isLivestream) {
} else if (isLivestreamMode) {
submitLabel = __('Create');
} else {
submitLabel = __('Upload');
@ -221,7 +318,7 @@ function PublishForm(props: Props) {
}
}, [thumbnail, resetThumbnailStatus]);
// Save current name of the editing claim
// Save previous name of the editing claim
useEffect(() => {
if (isStillEditing && (!prevName || !prevName.trim() === '')) {
if (name !== prevName) {
@ -263,17 +360,18 @@ function PublishForm(props: Props) {
}
}, [name, activeChannelName, resolveUri, updatePublishForm, checkAvailability]);
// because publish editingUri is channel_short/claim_long and we don't have that, resolve it.
useEffect(() => {
// because editingURI is lbry://channel_short/claim_long and that particular shape won't map to the claimId yet
if (editingURI) {
resolveUri(editingURI);
}
}, [editingURI, resolveUri]);
// set isMarkdownPost in publish form if so, also update isLivestreamPublish
useEffect(() => {
updatePublishForm({
isMarkdownPost: mode === PUBLISH_MODES.POST,
isLivestreamPublish: isLivestream,
isLivestreamPublish: isLivestreamMode,
});
}, [mode, updatePublishForm]);
@ -282,14 +380,15 @@ function PublishForm(props: Props) {
updatePublishForm({ channel: undefined });
// Anonymous livestreams aren't supported
if (isLivestream) {
if (isLivestreamMode) {
setMode(PUBLISH_MODES.FILE);
}
} else if (activeChannelName) {
updatePublishForm({ channel: activeChannelName });
}
}, [activeChannelName, incognito, updatePublishForm]);
}, [activeChannelName, incognito, updatePublishForm, isLivestreamMode]);
// set mode based on urlParams 'type'
useEffect(() => {
const _uploadType = uploadType && uploadType.toLowerCase();
@ -322,7 +421,7 @@ function PublishForm(props: Props) {
useEffect(() => {
if (!uploadType) return;
const newParams = new URLSearchParams();
newParams.set('type', mode.toLowerCase());
newParams.set(TYPE_PARAM, mode.toLowerCase());
replace({ search: newParams.toString() });
}, [mode, uploadType]);
@ -391,7 +490,7 @@ function PublishForm(props: Props) {
}
}
// Publish file
if (mode === PUBLISH_MODES.FILE || isLivestream) {
if (mode === PUBLISH_MODES.FILE || isLivestreamMode) {
runPublish = true;
}
@ -428,19 +527,23 @@ function PublishForm(props: Props) {
// Editing claim uri
return (
<div className="card-stack">
<ChannelSelect hideAnon={isLivestream} disabled={disabled} />
<ChannelSelect hideAnon={isLivestreamMode} disabled={disabled} />
<PublishFile
uri={uri}
uri={permanentUrl}
mode={mode}
fileMimeType={fileMimeType}
disabled={disabled || publishing}
inProgress={isInProgress}
setPublishMode={setMode}
setPrevFileText={setPrevFileText}
livestreamData={livestreamData}
subtitle={customSubtitle}
isCheckingLivestreams={isCheckingLivestreams}
checkLivestreams={checkLivestreams}
header={
<>
{MODES.map((modeName) => (
{AVAILABLE_MODES.map((modeName) => (
<Button
key={String(modeName)}
icon={modeName}
@ -460,7 +563,7 @@ function PublishForm(props: Props) {
{!publishing && (
<div className={classnames({ 'card--disabled': formDisabled })}>
{mode === PUBLISH_MODES.FILE && <PublishDescription disabled={formDisabled} />}
<Card actions={<SelectThumbnail />} />
<Card actions={<SelectThumbnail livestreamdData={livestreamData} />} />
<TagsSelect
suggestMature={!SIMPLE_SITE}
disableAutoFocus
@ -489,7 +592,7 @@ function PublishForm(props: Props) {
/>
<PublishBid disabled={isStillEditing || formDisabled} />
{!isLivestream && <PublishPrice disabled={formDisabled} />}
{!isLivestreamMode && <PublishPrice disabled={formDisabled} />}
<PublishAdditionalOptions disabled={formDisabled} />
</div>
)}

View file

@ -6,22 +6,29 @@ import {
selectPublishFormValues,
selectIsStillEditing,
selectMyChannelClaims,
makeSelectClaimIsStreamPlaceholder,
SETTINGS,
} from 'lbry-redux';
import { selectFfmpegStatus, makeSelectClientSetting } from 'redux/selectors/settings';
import { doPublishDesktop } from 'redux/actions/publish';
import { doSetClientSetting } from 'redux/actions/settings';
const select = (state) => ({
...selectPublishFormValues(state),
myChannels: selectMyChannelClaims(state),
isVid: makeSelectPublishFormValue('fileVid')(state),
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
publishing: makeSelectPublishFormValue('publishing')(state),
isStillEditing: selectIsStillEditing(state),
ffmpegStatus: selectFfmpegStatus(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
});
const select = (state, props) => {
const editingUri = makeSelectPublishFormValue('editingURI')(state);
return {
...selectPublishFormValues(state),
myChannels: selectMyChannelClaims(state),
isVid: makeSelectPublishFormValue('fileVid')(state),
publishSuccess: makeSelectPublishFormValue('publishSuccess')(state),
publishing: makeSelectPublishFormValue('publishing')(state),
remoteFile: makeSelectPublishFormValue('remoteFileUrl')(state),
isStillEditing: selectIsStillEditing(state),
ffmpegStatus: selectFfmpegStatus(state),
enablePublishPreview: makeSelectClientSetting(SETTINGS.ENABLE_PUBLISH_PREVIEW)(state),
isLivestreamClaim: makeSelectClaimIsStreamPlaceholder(editingUri)(state),
};
};
const perform = (dispatch) => ({
publish: (filePath, preview) => dispatch(doPublishDesktop(filePath, preview)),

View file

@ -43,6 +43,8 @@ type Props = {
myChannels: ?Array<ChannelClaim>,
publishSuccess: boolean,
publishing: boolean,
isLivestreamClaim: boolean,
remoteFile: string,
};
// class ModalPublishPreview extends React.PureComponent<Props> {
@ -74,10 +76,14 @@ const ModalPublishPreview = (props: Props) => {
publishing,
publish,
closeModal,
isLivestreamClaim,
remoteFile,
} = props;
const livestream =
(uri && isLivestreamClaim) ||
// $FlowFixMe
previewResponse.outputs[0] && previewResponse.outputs[0].value && !previewResponse.outputs[0].value.source;
(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
// @if TARGET='web'
React.useEffect(() => {
@ -122,7 +128,11 @@ const ModalPublishPreview = (props: Props) => {
const isOptimizeAvail = filePath && filePath !== '' && isVid && ffmpegStatus.available;
let modalTitle;
if (isStillEditing) {
modalTitle = __('Confirm Edit');
if (livestream) {
modalTitle = __('Confirm Update');
} else {
modalTitle = __('Confirm Edit');
}
} else if (livestream) {
modalTitle = __('Create Livestream');
} else {
@ -208,6 +218,7 @@ const ModalPublishPreview = (props: Props) => {
<table className="table table--condensed table--publish-preview">
<tbody>
{!livestream && !isMarkdownPost && createRow(__('File'), getFilePathName(filePath))}
{livestream && remoteFile && createRow(__('Replay'), __('Remote File Selected'))}
{isOptimizeAvail && createRow(__('Transcode'), optimize ? __('Yes') : __('No'))}
{createRow(__('Title'), title)}
{createRow(__('Description'), descriptionValue)}

View file

@ -205,3 +205,79 @@
.livestream__publish-intro {
margin-top: var(--spacing-l);
}
.table--livestream-data {
td:nth-of-type(1) {
width: 10%;
}
td:nth-of-type(2) {
width: 40%;
}
td:nth-of-type(3) {
width: 20%;
}
td:nth-of-type(4) {
width: 30%;
}
@media (max-width: $breakpoint-small) {
td:nth-of-type(1) {
max-width: 10%;
}
td:nth-of-type(2) {
width: 30%;
}
td:nth-of-type(3) {
width: 50%;
}
td:nth-of-type(4) {
display: none;
}
}
}
.livestream_thumb_container {
height: 4rem;
width: 100%;
display: flex;
flex-direction: row;
}
.livestream___thumb {
padding: var(--spacing-xxs);
}
.livestream__data-row {
cursor: pointer;
.radio {
cursor: pointer;
}
td {
padding-right: var(--spacing-m) !important;
@media (max-width: $breakpoint-small) {
padding: var(--spacing-xs) !important;
}
}
&:hover {
td {
.radio {
label::before {
cursor: pointer !important;
background-color: var(--color-input-toggle-bg-hover) !important;
}
}
label {
cursor: pointer !important;
}
}
input {
cursor: pointer;
background-color: var(--color-input-bg-selected);
}
}
}
.livestream__data-row--selected {
background-color: var(--color-input-bg-selected) !important;
}

View file

@ -40,12 +40,28 @@
td {
padding: 0.5rem 1rem;
}
th {
font-size: var(--font-xsmall);
color: var(--color-text-help);
padding-bottom: var(--spacing-xxxs);
@media (max-width: $breakpoint-small) {
padding-left: var(--spacing-xs) !important;
padding-right: var(--spacing-xs) !important;
}
}
}
th {
text-align: left;
}
td {
@media (max-width: $breakpoint-small) {
font-size: var(--font-small);
}
}
.table--fixed {
table-layout: fixed;
}

View file

@ -42,8 +42,10 @@ export default function apiPublishCallViaWeb(
if (fileField) {
body.append('file', fileField);
params.file_path = '__POST_FILE__';
delete params['remote_url'];
} else if (remoteUrl) {
body.append('remote_url', remoteUrl);
delete params['remote_url'];
}
const jsonPayload = JSON.stringify({