bring in livestream changes from odysee
This commit is contained in:
parent
5e1240df42
commit
7dc44194f9
24 changed files with 804 additions and 50 deletions
|
@ -117,6 +117,7 @@
|
|||
"electron-is-dev": "^0.3.0",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"electron-window-state": "^4.1.1",
|
||||
"emoji-dictionary": "^1.0.11",
|
||||
"eslint": "^5.15.2",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, selectMyChannelClaims, selectFetchingMyChannels } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
selectMyChannelClaims,
|
||||
selectFetchingMyChannels,
|
||||
} from 'lbry-redux';
|
||||
import { selectIsPostingComment } from 'redux/selectors/comments';
|
||||
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
||||
import { doCommentCreate } from 'redux/actions/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { CommentCreate } from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -14,12 +20,14 @@ const select = (state, props) => ({
|
|||
isFetchingChannels: selectFetchingMyChannels(state),
|
||||
isPostingComment: selectIsPostingComment(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
createComment: (comment, claimId, parentId) => dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri)),
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
setActiveChannel: claimId => dispatch(doSetActiveChannel(claimId)),
|
||||
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||
toast: (message) => dispatch(doToast({ message, isError: true })),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentCreate);
|
||||
|
|
|
@ -10,6 +10,16 @@ import usePersistedState from 'effects/use-persisted-state';
|
|||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { useHistory } from 'react-router';
|
||||
import type { ElementRef } from 'react';
|
||||
import emoji from 'emoji-dictionary';
|
||||
|
||||
const COMMENT_SLOW_MODE_SECONDS = 5;
|
||||
const LIVESTREAM_EMOJIS = [
|
||||
emoji.getUnicode('rocket'),
|
||||
emoji.getUnicode('jeans'),
|
||||
emoji.getUnicode('fire'),
|
||||
emoji.getUnicode('heart'),
|
||||
emoji.getUnicode('open_mouth'),
|
||||
];
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -25,6 +35,9 @@ type Props = {
|
|||
isPostingComment: boolean,
|
||||
activeChannel: string,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
livestream?: boolean,
|
||||
toast: (string) => void,
|
||||
claimIsMine: boolean,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
|
@ -40,11 +53,15 @@ export function CommentCreate(props: Props) {
|
|||
parentId,
|
||||
isPostingComment,
|
||||
activeChannelClaim,
|
||||
livestream,
|
||||
toast,
|
||||
claimIsMine,
|
||||
} = props;
|
||||
const buttonref: ElementRef<any> = React.useRef();
|
||||
const { push } = useHistory();
|
||||
const { claim_id: claimId } = claim;
|
||||
const [commentValue, setCommentValue] = React.useState('');
|
||||
const [lastCommentTime, setLastCommentTime] = React.useState();
|
||||
const [charCount, setCharCount] = useState(commentValue.length);
|
||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const hasChannels = channels && channels.length;
|
||||
|
@ -79,7 +96,18 @@ export function CommentCreate(props: Props) {
|
|||
|
||||
function handleSubmit() {
|
||||
if (activeChannelClaim && commentValue.length) {
|
||||
createComment(commentValue, claimId, parentId).then(res => {
|
||||
const timeUntilCanComment = !lastCommentTime
|
||||
? 0
|
||||
: lastCommentTime / 1000 - Date.now() / 1000 + COMMENT_SLOW_MODE_SECONDS;
|
||||
|
||||
if (livestream && !claimIsMine && timeUntilCanComment > 0) {
|
||||
toast(
|
||||
__('Slowmode is on. You can comment again in %time% seconds.', { time: Math.floor(timeUntilCanComment) })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
createComment(commentValue, claimId, parentId).then((res) => {
|
||||
if (res && res.signature) {
|
||||
setCommentValue('');
|
||||
|
||||
|
@ -144,6 +172,23 @@ export function CommentCreate(props: Props) {
|
|||
autoFocus={isReply}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
{livestream && hasChannels && (
|
||||
<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">
|
||||
<Button
|
||||
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,
|
||||
videoTheaterMode: boolean,
|
||||
isMarkdown?: boolean,
|
||||
livestream?: boolean,
|
||||
backout: {
|
||||
backLabel?: string,
|
||||
backNavDefault?: string,
|
||||
|
@ -49,6 +50,7 @@ function Page(props: Props) {
|
|||
backout,
|
||||
videoTheaterMode,
|
||||
isMarkdown = false,
|
||||
livestream,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -106,8 +108,9 @@ function Page(props: Props) {
|
|||
'main--full-width': fullWidthPage,
|
||||
'main--auth-page': authPage,
|
||||
'main--file-page': filePage,
|
||||
'main--theater-mode': isOnFilePage && videoTheaterMode,
|
||||
'main--markdown': isMarkdown,
|
||||
'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream,
|
||||
'main--livestream': livestream,
|
||||
})}
|
||||
>
|
||||
{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_COMPLETE = 'COMMENT_MODERATION_UN_BLOCK_COMPLETE';
|
||||
export const COMMENT_MODERATION_UN_BLOCK_FAILED = 'COMMENT_MODERATION_UN_BLOCK_FAILED';
|
||||
export const COMMENT_RECEIVED = 'COMMENT_RECEIVED';
|
||||
|
||||
// Blocked channels
|
||||
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 CreatorDashboardPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
const select = (state) => ({
|
||||
channels: selectMyChannelClaims(state),
|
||||
fetchingChannels: selectFetchingMyChannels(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 { connect } from 'react-redux';
|
||||
import { PAGE_SIZE } from 'constants/claim';
|
||||
import { LIVE_STREAM_TAG } from 'constants/livestream';
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimForUri,
|
||||
|
@ -10,6 +11,7 @@ import {
|
|||
normalizeURI,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimIsPending,
|
||||
makeSelectTagInClaimOrChannelForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||
|
@ -60,11 +62,13 @@ const select = (state, props) => {
|
|||
title: makeSelectTitleForUri(uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(uri)(state),
|
||||
claimIsPending: makeSelectClaimIsPending(uri)(state),
|
||||
// Change to !makeSelectClaimHasSource()
|
||||
isLivestream: makeSelectTagInClaimOrChannelForUri(uri, LIVE_STREAM_TAG)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
const perform = (dispatch) => ({
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ShowPage);
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Redirect } from 'react-router-dom';
|
|||
import Spinner from 'component/spinner';
|
||||
import ChannelPage from 'page/channel';
|
||||
import FilePage from 'page/file';
|
||||
import LivestreamPage from 'page/livestreamStream';
|
||||
import Page from 'component/page';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
|
@ -25,6 +26,7 @@ type Props = {
|
|||
title: string,
|
||||
claimIsMine: boolean,
|
||||
claimIsPending: boolean,
|
||||
isLivestream: boolean,
|
||||
};
|
||||
|
||||
function ShowPage(props: Props) {
|
||||
|
@ -38,6 +40,7 @@ function ShowPage(props: Props) {
|
|||
claimIsMine,
|
||||
isSubscribed,
|
||||
claimIsPending,
|
||||
isLivestream,
|
||||
} = props;
|
||||
|
||||
const signingChannel = claim && claim.signing_channel;
|
||||
|
@ -119,6 +122,10 @@ function ShowPage(props: Props) {
|
|||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLivestream) {
|
||||
innerContent = <LivestreamPage uri={uri} />;
|
||||
} else {
|
||||
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 analytics, { SHARE_INTERNAL } from 'analytics';
|
||||
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';
|
||||
|
||||
// @if TARGET='app'
|
||||
|
@ -115,10 +115,10 @@ export function doDownloadUpgrade() {
|
|||
const upgradeFilename = selectUpgradeFilename(state);
|
||||
|
||||
const options = {
|
||||
onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
|
||||
onProgress: (p) => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
|
||||
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
|
||||
* 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
|
||||
// the old logic.
|
||||
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
if (['win32', 'darwin'].includes(process.platform) || !!process.env.APPIMAGE) {
|
||||
// electron-updater behavior
|
||||
dispatch(doOpenModal(MODALS.AUTO_UPDATE_DOWNLOADED));
|
||||
|
@ -174,7 +174,7 @@ export function doClearUpgradeTimer() {
|
|||
}
|
||||
|
||||
export function doAutoUpdate() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.AUTO_UPDATE_DOWNLOADED,
|
||||
});
|
||||
|
@ -186,7 +186,7 @@ export function doAutoUpdate() {
|
|||
}
|
||||
|
||||
export function doAutoUpdateDeclined() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doClearUpgradeTimer());
|
||||
|
||||
dispatch({
|
||||
|
@ -267,7 +267,7 @@ export function doCheckUpgradeAvailable() {
|
|||
Initiate a timer that will check for an app upgrade every 10 minutes.
|
||||
*/
|
||||
export function doCheckUpgradeSubscribe() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
const checkUpgradeTimer = setInterval(() => dispatch(doCheckUpgradeAvailable()), CHECK_UPGRADE_INTERVAL);
|
||||
dispatch({
|
||||
type: ACTIONS.CHECK_UPGRADE_SUBSCRIBE,
|
||||
|
@ -277,7 +277,7 @@ export function doCheckUpgradeSubscribe() {
|
|||
}
|
||||
|
||||
export function doCheckDaemonVersion() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
// @if TARGET='app'
|
||||
Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => {
|
||||
// Avoid the incompatible daemon modal if running in dev mode
|
||||
|
@ -305,31 +305,31 @@ export function doCheckDaemonVersion() {
|
|||
}
|
||||
|
||||
export function doNotifyEncryptWallet() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doOpenModal(MODALS.WALLET_ENCRYPT));
|
||||
};
|
||||
}
|
||||
|
||||
export function doNotifyDecryptWallet() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doOpenModal(MODALS.WALLET_DECRYPT));
|
||||
};
|
||||
}
|
||||
|
||||
export function doNotifyUnlockWallet() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doOpenModal(MODALS.WALLET_UNLOCK));
|
||||
};
|
||||
}
|
||||
|
||||
export function doNotifyForgetPassword(props) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doOpenModal(MODALS.WALLET_PASSWORD_UNSAVE, props));
|
||||
};
|
||||
}
|
||||
|
||||
export function doAlertError(errorList) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doError(errorList));
|
||||
};
|
||||
}
|
||||
|
@ -364,7 +364,7 @@ export function doDaemonReady() {
|
|||
undefined,
|
||||
undefined,
|
||||
shareUsageData,
|
||||
status => {
|
||||
(status) => {
|
||||
const trendingAlgorithm =
|
||||
status &&
|
||||
status.wallet &&
|
||||
|
@ -397,7 +397,7 @@ export function doDaemonReady() {
|
|||
}
|
||||
|
||||
export function doClearCache() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
// Need to update this to work with new version of redux-persist
|
||||
// Leaving for now
|
||||
// const reducersToClear = whiteListedReducers.filter(reducerKey => reducerKey !== 'tags');
|
||||
|
@ -418,7 +418,7 @@ export function doQuit() {
|
|||
}
|
||||
|
||||
export function doQuitAnyDaemon() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
// @if TARGET='app'
|
||||
Lbry.stop()
|
||||
.catch(() => {
|
||||
|
@ -440,7 +440,7 @@ export function doQuitAnyDaemon() {
|
|||
}
|
||||
|
||||
export function doChangeVolume(volume) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.VOLUME_CHANGED,
|
||||
data: {
|
||||
|
@ -451,7 +451,7 @@ export function doChangeVolume(volume) {
|
|||
}
|
||||
|
||||
export function doChangeMute(muted) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.VOLUME_MUTED,
|
||||
data: {
|
||||
|
@ -528,7 +528,7 @@ export function doAnalyticsTagSync() {
|
|||
}
|
||||
|
||||
export function doAnaltyicsPurchaseEvent(fileInfo) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
let purchasePrice = fileInfo.purchase_receipt && fileInfo.purchase_receipt.amount;
|
||||
if (purchasePrice) {
|
||||
const purchaseInt = Number(Number(purchasePrice).toFixed(0));
|
||||
|
@ -544,7 +544,7 @@ export function doSignIn() {
|
|||
const notificationsEnabled = user.experimental_ui;
|
||||
|
||||
if (notificationsEnabled) {
|
||||
dispatch(doSocketConnect());
|
||||
dispatch(doNotificationSocketConnect());
|
||||
dispatch(doNotificationList());
|
||||
}
|
||||
|
||||
|
@ -667,7 +667,7 @@ export function doGetAndPopulatePreferences() {
|
|||
}
|
||||
|
||||
export function doHandleSyncComplete(error, hasNewData) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
if (!error) {
|
||||
dispatch(doGetAndPopulatePreferences());
|
||||
|
||||
|
@ -680,7 +680,7 @@ export function doHandleSyncComplete(error, hasNewData) {
|
|||
}
|
||||
|
||||
export function doSyncWithPreferences() {
|
||||
return dispatch => dispatch(doSyncLoop());
|
||||
return (dispatch) => dispatch(doSyncLoop());
|
||||
}
|
||||
|
||||
export function doToggleInterestedInYoutubeSync() {
|
||||
|
|
|
@ -2,44 +2,34 @@ import * as ACTIONS from 'constants/action_types';
|
|||
import { getAuthToken } from 'util/saved-passwords';
|
||||
import { doNotificationList } from 'redux/actions/notifications';
|
||||
|
||||
let socket = null;
|
||||
let sockets = {};
|
||||
let retryCount = 0;
|
||||
|
||||
export const doSocketConnect = () => dispatch => {
|
||||
const authToken = getAuthToken();
|
||||
if (!authToken) {
|
||||
console.error('Unable to connect to web socket because auth token is missing'); // eslint-disable-line
|
||||
return;
|
||||
}
|
||||
|
||||
export const doSocketConnect = (url, cb) => {
|
||||
function connectToSocket() {
|
||||
if (socket !== null) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
if (sockets[url] !== undefined && sockets[url] !== null) {
|
||||
sockets[url].close();
|
||||
sockets[url] = null;
|
||||
}
|
||||
|
||||
const timeToWait = retryCount ** 2 * 1000;
|
||||
setTimeout(() => {
|
||||
const url = `wss://api.lbry.com/subscribe?auth_token=${authToken}`;
|
||||
socket = new WebSocket(url);
|
||||
socket.onopen = e => {
|
||||
sockets[url] = new WebSocket(url);
|
||||
sockets[url].onopen = (e) => {
|
||||
retryCount = 0;
|
||||
console.log('\nConnected to WS \n\n'); // eslint-disable-line
|
||||
};
|
||||
socket.onmessage = e => {
|
||||
sockets[url].onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.type === 'pending_notification') {
|
||||
dispatch(doNotificationList());
|
||||
}
|
||||
cb(data);
|
||||
};
|
||||
|
||||
socket.onerror = e => {
|
||||
sockets[url].onerror = (e) => {
|
||||
console.error('websocket onerror', e); // eslint-disable-line
|
||||
// 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
|
||||
retryCount += 1;
|
||||
connectToSocket();
|
||||
|
@ -50,6 +40,35 @@ export const doSocketConnect = () => dispatch => {
|
|||
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 = () => ({
|
||||
type: ACTIONS.WS_DISCONNECT,
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
@import 'component/form-field';
|
||||
@import 'component/header';
|
||||
@import 'component/icon';
|
||||
@import 'component/livestream';
|
||||
@import 'component/main';
|
||||
@import 'component/markdown-editor';
|
||||
@import 'component/markdown-preview';
|
||||
|
|
|
@ -231,6 +231,11 @@
|
|||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.button--emoji {
|
||||
font-size: 1.25rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
display: flex;
|
||||
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 {
|
||||
@extend .main;
|
||||
|
||||
|
|
59
yarn.lock
59
yarn.lock
|
@ -4418,6 +4418,40 @@ elliptic@^6.0.0:
|
|||
minimalistic-assert "^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:
|
||||
version "7.0.3"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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:
|
||||
version "10.8.2"
|
||||
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"
|
||||
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:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||
|
|
Loading…
Reference in a new issue