bring in livestream changes from odysee
This commit is contained in:
parent
1ef44ce199
commit
73f593ddb3
24 changed files with 804 additions and 50 deletions
|
@ -117,6 +117,7 @@
|
||||||
"electron-is-dev": "^0.3.0",
|
"electron-is-dev": "^0.3.0",
|
||||||
"electron-webpack": "^2.8.2",
|
"electron-webpack": "^2.8.2",
|
||||||
"electron-window-state": "^4.1.1",
|
"electron-window-state": "^4.1.1",
|
||||||
|
"emoji-dictionary": "^1.0.11",
|
||||||
"eslint": "^5.15.2",
|
"eslint": "^5.15.2",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"eslint-config-standard": "^12.0.0",
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import { connect } from 'react-redux';
|
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 { selectIsPostingComment } from 'redux/selectors/comments';
|
||||||
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
||||||
import { doCommentCreate } from 'redux/actions/comments';
|
import { doCommentCreate } from 'redux/actions/comments';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { CommentCreate } from './view';
|
import { CommentCreate } from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
|
@ -14,12 +20,14 @@ const select = (state, props) => ({
|
||||||
isFetchingChannels: selectFetchingMyChannels(state),
|
isFetchingChannels: selectFetchingMyChannels(state),
|
||||||
isPostingComment: selectIsPostingComment(state),
|
isPostingComment: selectIsPostingComment(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
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)),
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
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);
|
export default connect(select, perform)(CommentCreate);
|
||||||
|
|
|
@ -10,6 +10,16 @@ import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import type { ElementRef } from 'react';
|
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 = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -25,6 +35,9 @@ type Props = {
|
||||||
isPostingComment: boolean,
|
isPostingComment: boolean,
|
||||||
activeChannel: string,
|
activeChannel: string,
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
|
livestream?: boolean,
|
||||||
|
toast: (string) => void,
|
||||||
|
claimIsMine: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommentCreate(props: Props) {
|
export function CommentCreate(props: Props) {
|
||||||
|
@ -40,11 +53,15 @@ export function CommentCreate(props: Props) {
|
||||||
parentId,
|
parentId,
|
||||||
isPostingComment,
|
isPostingComment,
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
|
livestream,
|
||||||
|
toast,
|
||||||
|
claimIsMine,
|
||||||
} = props;
|
} = props;
|
||||||
const buttonref: ElementRef<any> = React.useRef();
|
const buttonref: ElementRef<any> = React.useRef();
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const { claim_id: claimId } = claim;
|
const { claim_id: claimId } = claim;
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState('');
|
||||||
|
const [lastCommentTime, setLastCommentTime] = React.useState();
|
||||||
const [charCount, setCharCount] = useState(commentValue.length);
|
const [charCount, setCharCount] = useState(commentValue.length);
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const hasChannels = channels && channels.length;
|
const hasChannels = channels && channels.length;
|
||||||
|
@ -79,7 +96,18 @@ export function CommentCreate(props: Props) {
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (activeChannelClaim && commentValue.length) {
|
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) {
|
if (res && res.signature) {
|
||||||
setCommentValue('');
|
setCommentValue('');
|
||||||
|
|
||||||
|
@ -144,6 +172,23 @@ export function CommentCreate(props: Props) {
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
|
{livestream && hasChannels && (
|
||||||
|
<div className="livestream__emoji-actions">
|
||||||
|
{LIVESTREAM_EMOJIS.map((emoji) => (
|
||||||
|
<Button
|
||||||
|
key={emoji}
|
||||||
|
disabled={isPostingComment}
|
||||||
|
type="button"
|
||||||
|
button="alt"
|
||||||
|
className="button--emoji"
|
||||||
|
label={emoji}
|
||||||
|
onClick={() => {
|
||||||
|
setCommentValue(commentValue ? `${commentValue} ${emoji}` : emoji);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="section__actions section__actions--no-margin">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<Button
|
<Button
|
||||||
ref={buttonref}
|
ref={buttonref}
|
||||||
|
|
14
ui/component/livestreamComments/index.js
Normal file
14
ui/component/livestreamComments/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import { doCommentSocketConnect } from 'redux/actions/websocket';
|
||||||
|
import { doCommentList } from 'redux/actions/comments';
|
||||||
|
import { makeSelectTopLevelCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments';
|
||||||
|
import LivestreamFeed from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
comments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||||
|
fetchingComments: selectIsFetchingComments(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, { doCommentSocketConnect, doCommentList })(LivestreamFeed);
|
129
ui/component/livestreamComments/view.jsx
Normal file
129
ui/component/livestreamComments/view.jsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import CommentCreate from 'component/commentCreate';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
claim: ?StreamClaim,
|
||||||
|
activeViewers: number,
|
||||||
|
embed?: boolean,
|
||||||
|
doCommentSocketConnect: (string) => void,
|
||||||
|
doCommentList: (string) => void,
|
||||||
|
comments: Array<Comment>,
|
||||||
|
fetchingComments: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LivestreamFeed(props: Props) {
|
||||||
|
const { claim, uri, embed, doCommentSocketConnect, comments, doCommentList, fetchingComments } = props;
|
||||||
|
const commentsRef = React.createRef();
|
||||||
|
const hasScrolledComments = React.useRef();
|
||||||
|
const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false);
|
||||||
|
const claimId = claim && claim.claim_id;
|
||||||
|
const commentsLength = comments && comments.length;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (claimId) {
|
||||||
|
doCommentList(uri);
|
||||||
|
doCommentSocketConnect(claimId);
|
||||||
|
}
|
||||||
|
}, [claimId, uri]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const element = commentsRef.current;
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
if (element) {
|
||||||
|
const scrollHeight = element.scrollHeight - element.offsetHeight;
|
||||||
|
const isAtBottom = scrollHeight === element.scrollTop;
|
||||||
|
|
||||||
|
if (!isAtBottom) {
|
||||||
|
hasScrolledComments.current = true;
|
||||||
|
} else {
|
||||||
|
hasScrolledComments.current = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
|
if (commentsLength > 0) {
|
||||||
|
// Only update comment scroll if the user hasn't scrolled up to view old comments
|
||||||
|
// If they have, do nothing
|
||||||
|
if (!hasScrolledComments.current || !performedInitialScroll) {
|
||||||
|
element.scrollTop = element.scrollHeight - element.offsetHeight;
|
||||||
|
|
||||||
|
if (!performedInitialScroll) {
|
||||||
|
setPerformedInitialScroll(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (element) {
|
||||||
|
element.removeEventListener('scroll', handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [commentsRef, commentsLength, performedInitialScroll]);
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={__('Live discussion')}
|
||||||
|
smallTitle
|
||||||
|
className="livestream__discussion"
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
{fetchingComments && (
|
||||||
|
<div className="main--empty">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={commentsRef}
|
||||||
|
className={classnames('livestream__comments-wrapper', {
|
||||||
|
'livestream__comments-wrapper--with-height': commentsLength > 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!fetchingComments && comments.length > 0 ? (
|
||||||
|
<div className="livestream__comments">
|
||||||
|
{comments.map((comment) => (
|
||||||
|
<div key={comment.comment_id} className={classnames('livestream__comment')}>
|
||||||
|
{comment.channel_url ? (
|
||||||
|
<Button
|
||||||
|
target="_blank"
|
||||||
|
className={classnames('livestream__comment-author', {
|
||||||
|
'livestream__comment-author--streamer': claim.signing_channel.claim_id === comment.channel_id,
|
||||||
|
})}
|
||||||
|
navigate={comment.channel_url}
|
||||||
|
label={comment.channel_name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="livestream__comment-author">{comment.channel_name}</div>
|
||||||
|
)}
|
||||||
|
<MarkdownPreview content={comment.comment} simpleLinks />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="main--empty" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="livestream__comment-create">
|
||||||
|
<CommentCreate livestream bottom embed={embed} uri={uri} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
9
ui/component/livestreamLayout/index.js
Normal file
9
ui/component/livestreamLayout/index.js
Normal file
|
@ -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);
|
34
ui/component/livestreamLayout/view.jsx
Normal file
34
ui/component/livestreamLayout/view.jsx
Normal file
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="section card-stack">
|
||||||
|
<div className="file-render file-render--video livestream">
|
||||||
|
<div className="file-viewer">
|
||||||
|
<iframe src={`${BITWAVE_EMBED_URL}/${'doomtube'}?skin=odysee&autoplay=1`} scrolling="no" allowFullScreen />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FileTitle uri={uri} livestream activeViewers={activeViewers} />
|
||||||
|
</div>
|
||||||
|
<LivestreamComments uri={uri} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
9
ui/component/livestreamLink/index.js
Normal file
9
ui/component/livestreamLink/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectClaimForUri } from 'lbry-redux;';
|
||||||
|
import LivestreamLink from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
channelClaim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select)(LivestreamLink);
|
68
ui/component/livestreamLink/view.jsx
Normal file
68
ui/component/livestreamLink/view.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// @flow
|
||||||
|
import { LIVE_STREAM_TAG } from 'constants/livestream';
|
||||||
|
import React from 'react';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import ClaimPreview from 'component/claimPreview';
|
||||||
|
import { Lbry } from 'lbry-redux';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
channelClaim: ChannelClaim,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LivestreamLink(props: Props) {
|
||||||
|
const { channelClaim } = props;
|
||||||
|
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
|
||||||
|
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
|
||||||
|
const livestreamChannelId = channelClaim.claim_id;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
Lbry.claim_search({
|
||||||
|
channel_ids: [livestreamChannelId],
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [livestreamChannelId]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// function fetchIsStreaming() {
|
||||||
|
// // fetch(``)
|
||||||
|
// // .then((res) => res.json())
|
||||||
|
// // .then((res) => {
|
||||||
|
// // if (res && res.data && res.data.live) {
|
||||||
|
// // setIsLivestreaming(true);
|
||||||
|
// // } else {
|
||||||
|
// // setIsLivestreaming(false);
|
||||||
|
// // }
|
||||||
|
// // })
|
||||||
|
// // .catch((e) => {});
|
||||||
|
// }
|
||||||
|
// let interval;
|
||||||
|
// if (livestreamChannelId) {
|
||||||
|
// interval = setInterval(fetchIsStreaming, 5000);
|
||||||
|
// }
|
||||||
|
// return () => {
|
||||||
|
// if (interval) {
|
||||||
|
// clearInterval(interval);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
}, [livestreamChannelId]);
|
||||||
|
|
||||||
|
if (!livestreamClaim || !isLivestreaming) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="livestream__channel-link"
|
||||||
|
title="Live stream in progress"
|
||||||
|
actions={<ClaimPreview uri={livestreamClaim.canonical_url} livestream type="inline" />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ type Props = {
|
||||||
fullWidthPage: boolean,
|
fullWidthPage: boolean,
|
||||||
videoTheaterMode: boolean,
|
videoTheaterMode: boolean,
|
||||||
isMarkdown?: boolean,
|
isMarkdown?: boolean,
|
||||||
|
livestream?: boolean,
|
||||||
backout: {
|
backout: {
|
||||||
backLabel?: string,
|
backLabel?: string,
|
||||||
backNavDefault?: string,
|
backNavDefault?: string,
|
||||||
|
@ -49,6 +50,7 @@ function Page(props: Props) {
|
||||||
backout,
|
backout,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
isMarkdown = false,
|
isMarkdown = false,
|
||||||
|
livestream,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -106,8 +108,9 @@ function Page(props: Props) {
|
||||||
'main--full-width': fullWidthPage,
|
'main--full-width': fullWidthPage,
|
||||||
'main--auth-page': authPage,
|
'main--auth-page': authPage,
|
||||||
'main--file-page': filePage,
|
'main--file-page': filePage,
|
||||||
'main--theater-mode': isOnFilePage && videoTheaterMode,
|
|
||||||
'main--markdown': isMarkdown,
|
'main--markdown': isMarkdown,
|
||||||
|
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream,
|
||||||
|
'main--livestream': livestream,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -277,6 +277,7 @@ export const COMMENT_MODERATION_BLOCK_FAILED = 'COMMENT_MODERATION_BLOCK_FAILED'
|
||||||
export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_STARTED';
|
export const COMMENT_MODERATION_UN_BLOCK_STARTED = 'COMMENT_MODERATION_UN_BLOCK_STARTED';
|
||||||
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
|
export const COMMENT_MODERATION_UN_BLOCK_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
|
||||||
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
||||||
|
export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
|
||||||
|
|
||||||
// Blocked channels
|
// Blocked channels
|
||||||
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';
|
||||||
|
|
4
ui/constants/livestream.js
Normal file
4
ui/constants/livestream.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export const LIVE_STREAM_TAG = 'odysee-livestream';
|
||||||
|
|
||||||
|
export const BITWAVE_EMBED_URL = 'https://bitwave.tv/embed';
|
||||||
|
export const BITWAVE_API = 'https://api.bitwave.tv/v1/channels';
|
|
@ -4,7 +4,7 @@ import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { doSetActiveChannel } from 'redux/actions/app';
|
import { doSetActiveChannel } from 'redux/actions/app';
|
||||||
import CreatorDashboardPage from './view';
|
import CreatorDashboardPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
channels: selectMyChannelClaims(state),
|
channels: selectMyChannelClaims(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim: selectActiveChannelClaim(state),
|
||||||
|
|
18
ui/page/livestreamStream/index.js
Normal file
18
ui/page/livestreamStream/index.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doResolveUri } from 'lbry-redux';
|
||||||
|
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 LivestreamPage from './view';
|
||||||
|
|
||||||
|
const select = (state) => ({
|
||||||
|
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
|
||||||
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doSetPlayingUri,
|
||||||
|
doResolveUri,
|
||||||
|
doUserSetReferrer,
|
||||||
|
})(LivestreamPage);
|
114
ui/page/livestreamStream/view.jsx
Normal file
114
ui/page/livestreamStream/view.jsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
// @flow
|
||||||
|
// import { LIVE_STREAM_TAG, BITWAVE_API } from 'constants/livestream';
|
||||||
|
import React from 'react';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import LivestreamLayout from 'component/livestreamLayout';
|
||||||
|
import analytics from 'analytics';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
claim: StreamClaim,
|
||||||
|
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||||
|
isAuthenticated: boolean,
|
||||||
|
doUserSetReferrer: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LivestreamPage(props: Props) {
|
||||||
|
const { uri, claim, doSetPlayingUri, isAuthenticated, doUserSetReferrer } = props;
|
||||||
|
const [activeViewers, setActiveViewers] = React.useState(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// function checkIsLive() {
|
||||||
|
// fetch(`${BITWAVE_API}/`)
|
||||||
|
// .then((res) => res.json())
|
||||||
|
// .then((res) => {
|
||||||
|
// if (!res || !res.data) {
|
||||||
|
// setIsLive(false);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// setActiveViewers(res.data.viewCount);
|
||||||
|
// if (res.data.live) {
|
||||||
|
// setDisplayCountdown(false);
|
||||||
|
// setIsLive(true);
|
||||||
|
// setIsFetching(false);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // Not live, but see if we can display the countdown;
|
||||||
|
// const scheduledTime = res.data.scheduled;
|
||||||
|
// if (scheduledTime) {
|
||||||
|
// const scheduledDate = new Date(scheduledTime);
|
||||||
|
// const lastLiveTimestamp = res.data.timestamp;
|
||||||
|
// let isLivestreamOver = false;
|
||||||
|
// if (lastLiveTimestamp) {
|
||||||
|
// const timestampDate = new Date(lastLiveTimestamp);
|
||||||
|
// isLivestreamOver = timestampDate.getTime() > scheduledDate.getTime();
|
||||||
|
// }
|
||||||
|
// if (isLivestreamOver) {
|
||||||
|
// setDisplayCountdown(false);
|
||||||
|
// setIsLive(false);
|
||||||
|
// } else {
|
||||||
|
// const datePlusTenMinuteBuffer = scheduledDate.setMinutes(10, 0, 0);
|
||||||
|
// const isInFuture = Date.now() < datePlusTenMinuteBuffer;
|
||||||
|
// if (isInFuture) {
|
||||||
|
// setDisplayCountdown(true);
|
||||||
|
// setIsLive(false);
|
||||||
|
// } else {
|
||||||
|
// setDisplayCountdown(false);
|
||||||
|
// setIsLive(false);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// setIsFetching(false);
|
||||||
|
// } else {
|
||||||
|
// // Offline and no countdown happening
|
||||||
|
// setIsLive(false);
|
||||||
|
// setDisplayCountdown(false);
|
||||||
|
// setActiveViewers(0);
|
||||||
|
// setIsFetching(false);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// let interval;
|
||||||
|
// checkIsLive();
|
||||||
|
// if (uri) {
|
||||||
|
// interval = setInterval(checkIsLive, 10000);
|
||||||
|
// }
|
||||||
|
// return () => {
|
||||||
|
// if (interval) {
|
||||||
|
// clearInterval(interval);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
}, [uri]);
|
||||||
|
|
||||||
|
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 bitwave iframe
|
||||||
|
doSetPlayingUri({ uri: null });
|
||||||
|
}, [doSetPlayingUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page className="file-page" filePage livestream>
|
||||||
|
<LivestreamLayout uri={uri} activeViewers={activeViewers} />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { DOMAIN } from 'config';
|
import { DOMAIN } from 'config';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { PAGE_SIZE } from 'constants/claim';
|
import { PAGE_SIZE } from 'constants/claim';
|
||||||
|
import { LIVE_STREAM_TAG } from 'constants/livestream';
|
||||||
import {
|
import {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
|
@ -10,6 +11,7 @@ import {
|
||||||
normalizeURI,
|
normalizeURI,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
makeSelectClaimIsPending,
|
makeSelectClaimIsPending,
|
||||||
|
makeSelectTagInClaimOrChannelForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
|
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
|
@ -60,11 +62,13 @@ 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),
|
||||||
|
// Change to !makeSelectClaimHasSource()
|
||||||
|
isLivestream: makeSelectTagInClaimOrChannelForUri(uri, LIVE_STREAM_TAG)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = (dispatch) => ({
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ShowPage);
|
export default connect(select, perform)(ShowPage);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Redirect } from 'react-router-dom';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import ChannelPage from 'page/channel';
|
import ChannelPage from 'page/channel';
|
||||||
import FilePage from 'page/file';
|
import FilePage from 'page/file';
|
||||||
|
import LivestreamPage from 'page/livestreamStream';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
@ -25,6 +26,7 @@ type Props = {
|
||||||
title: string,
|
title: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
claimIsPending: boolean,
|
claimIsPending: boolean,
|
||||||
|
isLivestream: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShowPage(props: Props) {
|
function ShowPage(props: Props) {
|
||||||
|
@ -38,6 +40,7 @@ function ShowPage(props: Props) {
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
isSubscribed,
|
isSubscribed,
|
||||||
claimIsPending,
|
claimIsPending,
|
||||||
|
isLivestream,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const signingChannel = claim && claim.signing_channel;
|
const signingChannel = claim && claim.signing_channel;
|
||||||
|
@ -119,6 +122,10 @@ function ShowPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLivestream) {
|
||||||
|
innerContent = <LivestreamPage uri={uri} />;
|
||||||
} else {
|
} else {
|
||||||
innerContent = <FilePage uri={uri} location={location} />;
|
innerContent = <FilePage uri={uri} location={location} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ import { doAuthenticate } from 'redux/actions/user';
|
||||||
import { lbrySettings as config, version as appVersion } from 'package.json';
|
import { lbrySettings as config, version as appVersion } from 'package.json';
|
||||||
import analytics, { SHARE_INTERNAL } from 'analytics';
|
import analytics, { SHARE_INTERNAL } from 'analytics';
|
||||||
import { doSignOutCleanup } from 'util/saved-passwords';
|
import { doSignOutCleanup } from 'util/saved-passwords';
|
||||||
import { doSocketConnect } from 'redux/actions/websocket';
|
import { doNotificationSocketConnect } from 'redux/actions/websocket';
|
||||||
import { stringifyServerParam, shouldSetSetting } from 'util/sync-settings';
|
import { stringifyServerParam, shouldSetSetting } from 'util/sync-settings';
|
||||||
|
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
@ -115,10 +115,10 @@ export function doDownloadUpgrade() {
|
||||||
const upgradeFilename = selectUpgradeFilename(state);
|
const upgradeFilename = selectUpgradeFilename(state);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
|
onProgress: (p) => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
|
||||||
directory: dir,
|
directory: dir,
|
||||||
};
|
};
|
||||||
download(remote.getCurrentWindow(), selectUpdateUrl(state), options).then(downloadItem => {
|
download(remote.getCurrentWindow(), selectUpdateUrl(state), options).then((downloadItem) => {
|
||||||
/**
|
/**
|
||||||
* TODO: get the download path directly from the download object. It should just be
|
* TODO: get the download path directly from the download object. It should just be
|
||||||
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
|
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
|
||||||
|
@ -149,7 +149,7 @@ export function doDownloadUpgradeRequested() {
|
||||||
// This will probably be reorganized once we get auto-update going on Linux and remove
|
// This will probably be reorganized once we get auto-update going on Linux and remove
|
||||||
// the old logic.
|
// the old logic.
|
||||||
|
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
if (['win32', 'darwin'].includes(process.platform) || !!process.env.APPIMAGE) {
|
if (['win32', 'darwin'].includes(process.platform) || !!process.env.APPIMAGE) {
|
||||||
// electron-updater behavior
|
// electron-updater behavior
|
||||||
dispatch(doOpenModal(MODALS.AUTO_UPDATE_DOWNLOADED));
|
dispatch(doOpenModal(MODALS.AUTO_UPDATE_DOWNLOADED));
|
||||||
|
@ -174,7 +174,7 @@ export function doClearUpgradeTimer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAutoUpdate() {
|
export function doAutoUpdate() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.AUTO_UPDATE_DOWNLOADED,
|
type: ACTIONS.AUTO_UPDATE_DOWNLOADED,
|
||||||
});
|
});
|
||||||
|
@ -186,7 +186,7 @@ export function doAutoUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAutoUpdateDeclined() {
|
export function doAutoUpdateDeclined() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doClearUpgradeTimer());
|
dispatch(doClearUpgradeTimer());
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -267,7 +267,7 @@ export function doCheckUpgradeAvailable() {
|
||||||
Initiate a timer that will check for an app upgrade every 10 minutes.
|
Initiate a timer that will check for an app upgrade every 10 minutes.
|
||||||
*/
|
*/
|
||||||
export function doCheckUpgradeSubscribe() {
|
export function doCheckUpgradeSubscribe() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
const checkUpgradeTimer = setInterval(() => dispatch(doCheckUpgradeAvailable()), CHECK_UPGRADE_INTERVAL);
|
const checkUpgradeTimer = setInterval(() => dispatch(doCheckUpgradeAvailable()), CHECK_UPGRADE_INTERVAL);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.CHECK_UPGRADE_SUBSCRIBE,
|
type: ACTIONS.CHECK_UPGRADE_SUBSCRIBE,
|
||||||
|
@ -277,7 +277,7 @@ export function doCheckUpgradeSubscribe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCheckDaemonVersion() {
|
export function doCheckDaemonVersion() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => {
|
Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => {
|
||||||
// Avoid the incompatible daemon modal if running in dev mode
|
// Avoid the incompatible daemon modal if running in dev mode
|
||||||
|
@ -305,31 +305,31 @@ export function doCheckDaemonVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doNotifyEncryptWallet() {
|
export function doNotifyEncryptWallet() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doOpenModal(MODALS.WALLET_ENCRYPT));
|
dispatch(doOpenModal(MODALS.WALLET_ENCRYPT));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doNotifyDecryptWallet() {
|
export function doNotifyDecryptWallet() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doOpenModal(MODALS.WALLET_DECRYPT));
|
dispatch(doOpenModal(MODALS.WALLET_DECRYPT));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doNotifyUnlockWallet() {
|
export function doNotifyUnlockWallet() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doOpenModal(MODALS.WALLET_UNLOCK));
|
dispatch(doOpenModal(MODALS.WALLET_UNLOCK));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doNotifyForgetPassword(props) {
|
export function doNotifyForgetPassword(props) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doOpenModal(MODALS.WALLET_PASSWORD_UNSAVE, props));
|
dispatch(doOpenModal(MODALS.WALLET_PASSWORD_UNSAVE, props));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAlertError(errorList) {
|
export function doAlertError(errorList) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doError(errorList));
|
dispatch(doError(errorList));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -364,7 +364,7 @@ export function doDaemonReady() {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
shareUsageData,
|
shareUsageData,
|
||||||
status => {
|
(status) => {
|
||||||
const trendingAlgorithm =
|
const trendingAlgorithm =
|
||||||
status &&
|
status &&
|
||||||
status.wallet &&
|
status.wallet &&
|
||||||
|
@ -397,7 +397,7 @@ export function doDaemonReady() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doClearCache() {
|
export function doClearCache() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
// Need to update this to work with new version of redux-persist
|
// Need to update this to work with new version of redux-persist
|
||||||
// Leaving for now
|
// Leaving for now
|
||||||
// const reducersToClear = whiteListedReducers.filter(reducerKey => reducerKey !== 'tags');
|
// const reducersToClear = whiteListedReducers.filter(reducerKey => reducerKey !== 'tags');
|
||||||
|
@ -418,7 +418,7 @@ export function doQuit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doQuitAnyDaemon() {
|
export function doQuitAnyDaemon() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
Lbry.stop()
|
Lbry.stop()
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
@ -440,7 +440,7 @@ export function doQuitAnyDaemon() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doChangeVolume(volume) {
|
export function doChangeVolume(volume) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.VOLUME_CHANGED,
|
type: ACTIONS.VOLUME_CHANGED,
|
||||||
data: {
|
data: {
|
||||||
|
@ -451,7 +451,7 @@ export function doChangeVolume(volume) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doChangeMute(muted) {
|
export function doChangeMute(muted) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.VOLUME_MUTED,
|
type: ACTIONS.VOLUME_MUTED,
|
||||||
data: {
|
data: {
|
||||||
|
@ -528,7 +528,7 @@ export function doAnalyticsTagSync() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doAnaltyicsPurchaseEvent(fileInfo) {
|
export function doAnaltyicsPurchaseEvent(fileInfo) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
let purchasePrice = fileInfo.purchase_receipt && fileInfo.purchase_receipt.amount;
|
let purchasePrice = fileInfo.purchase_receipt && fileInfo.purchase_receipt.amount;
|
||||||
if (purchasePrice) {
|
if (purchasePrice) {
|
||||||
const purchaseInt = Number(Number(purchasePrice).toFixed(0));
|
const purchaseInt = Number(Number(purchasePrice).toFixed(0));
|
||||||
|
@ -544,7 +544,7 @@ export function doSignIn() {
|
||||||
const notificationsEnabled = user.experimental_ui;
|
const notificationsEnabled = user.experimental_ui;
|
||||||
|
|
||||||
if (notificationsEnabled) {
|
if (notificationsEnabled) {
|
||||||
dispatch(doSocketConnect());
|
dispatch(doNotificationSocketConnect());
|
||||||
dispatch(doNotificationList());
|
dispatch(doNotificationList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -667,7 +667,7 @@ export function doGetAndPopulatePreferences() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doHandleSyncComplete(error, hasNewData) {
|
export function doHandleSyncComplete(error, hasNewData) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
if (!error) {
|
if (!error) {
|
||||||
dispatch(doGetAndPopulatePreferences());
|
dispatch(doGetAndPopulatePreferences());
|
||||||
|
|
||||||
|
@ -680,7 +680,7 @@ export function doHandleSyncComplete(error, hasNewData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doSyncWithPreferences() {
|
export function doSyncWithPreferences() {
|
||||||
return dispatch => dispatch(doSyncLoop());
|
return (dispatch) => dispatch(doSyncLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doToggleInterestedInYoutubeSync() {
|
export function doToggleInterestedInYoutubeSync() {
|
||||||
|
|
|
@ -2,44 +2,34 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import { getAuthToken } from 'util/saved-passwords';
|
import { getAuthToken } from 'util/saved-passwords';
|
||||||
import { doNotificationList } from 'redux/actions/notifications';
|
import { doNotificationList } from 'redux/actions/notifications';
|
||||||
|
|
||||||
let socket = null;
|
let sockets = {};
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
|
|
||||||
export const doSocketConnect = () => dispatch => {
|
export const doSocketConnect = (url, cb) => {
|
||||||
const authToken = getAuthToken();
|
|
||||||
if (!authToken) {
|
|
||||||
console.error('Unable to connect to web socket because auth token is missing'); // eslint-disable-line
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectToSocket() {
|
function connectToSocket() {
|
||||||
if (socket !== null) {
|
if (sockets[url] !== undefined && sockets[url] !== null) {
|
||||||
socket.close();
|
sockets[url].close();
|
||||||
socket = null;
|
sockets[url] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeToWait = retryCount ** 2 * 1000;
|
const timeToWait = retryCount ** 2 * 1000;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const url = `wss://api.lbry.com/subscribe?auth_token=${authToken}`;
|
sockets[url] = new WebSocket(url);
|
||||||
socket = new WebSocket(url);
|
sockets[url].onopen = (e) => {
|
||||||
socket.onopen = e => {
|
|
||||||
retryCount = 0;
|
retryCount = 0;
|
||||||
console.log('\nConnected to WS \n\n'); // eslint-disable-line
|
console.log('\nConnected to WS \n\n'); // eslint-disable-line
|
||||||
};
|
};
|
||||||
socket.onmessage = e => {
|
sockets[url].onmessage = (e) => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
|
cb(data);
|
||||||
if (data.type === 'pending_notification') {
|
|
||||||
dispatch(doNotificationList());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onerror = e => {
|
sockets[url].onerror = (e) => {
|
||||||
console.error('websocket onerror', e); // eslint-disable-line
|
console.error('websocket onerror', e); // eslint-disable-line
|
||||||
// onerror and onclose will both fire, so nothing is needed here
|
// onerror and onclose will both fire, so nothing is needed here
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.onclose = e => {
|
sockets[url].onclose = (e) => {
|
||||||
console.error('websocket onclose', e); // eslint-disable-line
|
console.error('websocket onclose', e); // eslint-disable-line
|
||||||
retryCount += 1;
|
retryCount += 1;
|
||||||
connectToSocket();
|
connectToSocket();
|
||||||
|
@ -50,6 +40,35 @@ export const doSocketConnect = () => dispatch => {
|
||||||
connectToSocket();
|
connectToSocket();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const doNotificationSocketConnect = () => (dispatch) => {
|
||||||
|
const authToken = getAuthToken();
|
||||||
|
if (!authToken) {
|
||||||
|
console.error('Unable to connect to web socket because auth token is missing'); // eslint-disable-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `wss://api.lbry.com/subscribe?auth_token=${authToken}`;
|
||||||
|
doSocketConnect(url, (data) => {
|
||||||
|
if (data.type === 'pending_notification') {
|
||||||
|
dispatch(doNotificationList());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doCommentSocketConnect = (claimId) => (dispatch) => {
|
||||||
|
const url = `wss://comments.lbry.com/api/v2/live-chat/subscribe?subscription_id=${claimId}`;
|
||||||
|
|
||||||
|
doSocketConnect(url, (response) => {
|
||||||
|
if (response.type === 'delta') {
|
||||||
|
const newComment = response.data.comment;
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_RECEIVED,
|
||||||
|
data: { comment: newComment, claimId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const doSocketDisconnect = () => ({
|
export const doSocketDisconnect = () => ({
|
||||||
type: ACTIONS.WS_DISCONNECT,
|
type: ACTIONS.WS_DISCONNECT,
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
@import 'component/form-field';
|
@import 'component/form-field';
|
||||||
@import 'component/header';
|
@import 'component/header';
|
||||||
@import 'component/icon';
|
@import 'component/icon';
|
||||||
|
@import 'component/livestream';
|
||||||
@import 'component/main';
|
@import 'component/main';
|
||||||
@import 'component/markdown-editor';
|
@import 'component/markdown-editor';
|
||||||
@import 'component/markdown-preview';
|
@import 'component/markdown-preview';
|
||||||
|
|
|
@ -231,6 +231,11 @@
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button--emoji {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
border-radius: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.button__content {
|
.button__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
198
ui/scss/component/_livestream.scss
Normal file
198
ui/scss/component/_livestream.scss
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
.livestream {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding-top: var(--aspect-ratio-standard);
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__discussion {
|
||||||
|
min-height: 0%;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
width: 35rem;
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comments-wrapper {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comments-wrapper--with-height {
|
||||||
|
height: 40vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
font-size: var(--font-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comment {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
> :first-child {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comment-author {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comment-author--streamer {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comment-create {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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__emoji-actions {
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__embed-page {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.file-viewer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__embed-wrapper {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #000000;
|
||||||
|
|
||||||
|
.livestream {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__embed-countdown {
|
||||||
|
@extend .livestream__embed-wrapper;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__embed {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__embed-comments {
|
||||||
|
width: 30vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.livestream__discussion {
|
||||||
|
height: 100vh;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__main-actions {
|
||||||
|
height: 100%;
|
||||||
|
width: 30vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comments-wrapper--with-height {
|
||||||
|
height: calc(100% - 200px - (var(--spacing-l)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
|
@ -169,6 +169,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main--livestream {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
.main--full-width {
|
.main--full-width {
|
||||||
@extend .main;
|
@extend .main;
|
||||||
|
|
||||||
|
|
59
yarn.lock
59
yarn.lock
|
@ -4418,6 +4418,40 @@ elliptic@^6.0.0:
|
||||||
minimalistic-assert "^1.0.1"
|
minimalistic-assert "^1.0.1"
|
||||||
minimalistic-crypto-utils "^1.0.1"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
|
emoji-chars@^1.0.0:
|
||||||
|
version "1.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-chars/-/emoji-chars-1.0.12.tgz#432591cac3eafd58beb80f8b5b5e950a96c8de17"
|
||||||
|
integrity sha512-1t7WbkKzQ1hV4dHWM4u8g0SFHSAbxx+8o/lvXisDLTesljSFaxl2wLgMtx4wH922sNcIuLbFty/AuqUDJORd1A==
|
||||||
|
dependencies:
|
||||||
|
emoji-unicode-map "^1.0.0"
|
||||||
|
|
||||||
|
emoji-dictionary@^1.0.11:
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-dictionary/-/emoji-dictionary-1.0.11.tgz#dc3463e4d768a1e11ffabc9362ac0092472bd308"
|
||||||
|
integrity sha512-pVTiN0StAU2nYy+BtcX/88DavMDjUcONIA6Qsg7/IyDq8xOsRFuC49F7XLUPr7Shlz4bt0/RAqPjuqjpsj3vbA==
|
||||||
|
dependencies:
|
||||||
|
emoji-chars "^1.0.0"
|
||||||
|
emoji-name-map "^1.0.0"
|
||||||
|
emoji-names "^1.0.1"
|
||||||
|
emoji-unicode-map "^1.0.0"
|
||||||
|
emojilib "^2.0.2"
|
||||||
|
|
||||||
|
emoji-name-map@^1.0.0, emoji-name-map@^1.1.0:
|
||||||
|
version "1.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-name-map/-/emoji-name-map-1.2.9.tgz#32a0788e748d9c7185a29a000102fb263fe2892d"
|
||||||
|
integrity sha512-MSM8y6koSqh/2uEMI2VoKA+Ac0qL5RkgFGP/pzL6n5FOrOJ7FOZFxgs7+uNpqA+AT+WmdbMPXkd3HnFXXdz4AA==
|
||||||
|
dependencies:
|
||||||
|
emojilib "^2.0.2"
|
||||||
|
iterate-object "^1.3.1"
|
||||||
|
map-o "^2.0.1"
|
||||||
|
|
||||||
|
emoji-names@^1.0.1:
|
||||||
|
version "1.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-names/-/emoji-names-1.0.12.tgz#4789d72af311e3a9b0343fe8752e77420e32f8f9"
|
||||||
|
integrity sha512-ABXVMPYU9h1/0lNNE9VaT9OxxWXXAv/By8gVMzQYIx7jrhWjyLFVyC34CAN+EP/1e+5WZCklvClo5KSklPCAeg==
|
||||||
|
dependencies:
|
||||||
|
emoji-name-map "^1.0.0"
|
||||||
|
|
||||||
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
|
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||||
|
@ -4426,6 +4460,19 @@ emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
|
||||||
|
emoji-unicode-map@^1.0.0:
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-unicode-map/-/emoji-unicode-map-1.1.11.tgz#bc3f726abb186cc4f156316d69322f9067e30947"
|
||||||
|
integrity sha512-GWcWILFyDfR8AU7FRLhKk0lnvcljoEIXejg+XY3Ogz6/ELaQLMo0m4d9R3i79ikIULVEysHBGPsOEcjcFxtN+w==
|
||||||
|
dependencies:
|
||||||
|
emoji-name-map "^1.1.0"
|
||||||
|
iterate-object "^1.3.1"
|
||||||
|
|
||||||
|
emojilib@^2.0.2:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e"
|
||||||
|
integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==
|
||||||
|
|
||||||
emojis-list@^2.0.0:
|
emojis-list@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||||
|
@ -6676,6 +6723,11 @@ isurl@^1.0.0-alpha5:
|
||||||
has-to-string-tag-x "^1.2.0"
|
has-to-string-tag-x "^1.2.0"
|
||||||
is-object "^1.0.1"
|
is-object "^1.0.1"
|
||||||
|
|
||||||
|
iterate-object@^1.3.0, iterate-object@^1.3.1:
|
||||||
|
version "1.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/iterate-object/-/iterate-object-1.3.4.tgz#fa50b1d9e58e340a7dd6b4c98c8a5e182e790096"
|
||||||
|
integrity sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==
|
||||||
|
|
||||||
jake@^10.6.1:
|
jake@^10.6.1:
|
||||||
version "10.8.2"
|
version "10.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
|
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b"
|
||||||
|
@ -7319,6 +7371,13 @@ map-cache@^0.2.2:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
|
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
|
||||||
|
|
||||||
|
map-o@^2.0.1:
|
||||||
|
version "2.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/map-o/-/map-o-2.0.10.tgz#b656a75cead939a936c338fb5df8c9d55f0dbb23"
|
||||||
|
integrity sha512-BxazE81fVByHWasyXhqKeo2m7bFKYu+ZbEfiuexMOnklXW+tzDvnlTi/JaklEeuuwqcqJzPaf9q+TWptSGXeLg==
|
||||||
|
dependencies:
|
||||||
|
iterate-object "^1.3.0"
|
||||||
|
|
||||||
map-stream@~0.1.0:
|
map-stream@~0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||||
|
|
Loading…
Reference in a new issue