Squashed commit of the following:
commit 9703e29e9702c3a9339a984268da3c2f78f77afa Author: saltrafael <oversaid_logodaedaly@slmail.me> Date: Wed Nov 3 08:55:33 2021 -0300 Refactor Livestream Page commit cc0fbaa79dacd196c62806b1abe683dc30a9c59d Author: saltrafael <oversaid_logodaedaly@slmail.me> Date: Wed Nov 3 08:51:14 2021 -0300 Refactor livestreamComment component and split CSS commit d90066140e28e0e91b315e8d0e33f9ce0b9fb7bc Author: saltrafael <oversaid_logodaedaly@slmail.me> Date: Wed Nov 3 08:45:51 2021 -0300 Refactor Comment Editing commit 10dd53568b633ca976a61fb3292c2f3c09724945 Author: saltrafael <oversaid_logodaedaly@slmail.me> Date: Wed Nov 3 08:38:26 2021 -0300 Refactor Comment components and split CSS
This commit is contained in:
parent
2974b35d21
commit
d80be47fad
39 changed files with 1063 additions and 1320 deletions
|
@ -5,46 +5,47 @@ import {
|
||||||
makeSelectThumbnailForUri,
|
makeSelectThumbnailForUri,
|
||||||
selectMyChannelClaims,
|
selectMyChannelClaims,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import {
|
import {
|
||||||
selectLinkedCommentAncestors,
|
selectLinkedCommentAncestors,
|
||||||
selectOthersReactsForComment,
|
selectOthersReactsForComment,
|
||||||
makeSelectTotalReplyPagesForParentId,
|
makeSelectTotalReplyPagesForParentId,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
|
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||||
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectPlayingUri } from 'redux/selectors/content';
|
import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
import Comment from './view';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import CommentView from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
const { channel_url: authorUri, comment_id: commentId } = props.comment;
|
||||||
|
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
const reactionKey = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
channelIsBlocked: authorUri && makeSelectChannelIsMuted(authorUri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
|
||||||
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
|
||||||
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
|
||||||
activeChannelClaim,
|
|
||||||
myChannels: selectMyChannelClaims(state),
|
|
||||||
playingUri: selectPlayingUri(state),
|
|
||||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
|
||||||
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||||
totalReplyPages: makeSelectTotalReplyPagesForParentId(props.commentId)(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
|
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
||||||
|
playingUri: selectPlayingUri(state),
|
||||||
|
stakedLevel: makeSelectStakedLevelForChannelUri(authorUri)(state),
|
||||||
|
thumbnail: authorUri && makeSelectThumbnailForUri(authorUri)(state),
|
||||||
|
totalReplyPages: makeSelectTotalReplyPagesForParentId(commentId)(state),
|
||||||
|
userCanComment: selectUserVerifiedEmail(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
|
updateComment: (editedComment) => dispatch(doCommentUpdate(ownProps.comment.comment_id, editedComment)),
|
||||||
fetchReplies: (uri, parentId, page, pageSize, sortBy) =>
|
fetchReplies: (page, pageSize, sortBy) =>
|
||||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
dispatch(doCommentList(ownProps.uri, ownProps.comment.comment_id, page, pageSize, sortBy)),
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(Comment);
|
export default connect(select, perform)(CommentView);
|
||||||
|
|
|
@ -1,120 +1,100 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import 'scss/component/_comments.scss';
|
||||||
import * as PAGES from 'constants/pages';
|
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import * as KEYCODES from 'constants/keycodes';
|
|
||||||
import { COMMENT_HIGHLIGHTED } from 'constants/classnames';
|
|
||||||
import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
|
|
||||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
|
||||||
import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { parseURI } from 'util/lbryURI';
|
|
||||||
import DateTime from 'component/dateTime';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import Expandable from 'component/expandable';
|
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
|
||||||
import Tooltip from 'component/common/tooltip';
|
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
|
||||||
import { Menu, MenuButton } from '@reach/menu-button';
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import { parseSticker } from 'util/comments';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { parseURI } from 'util/lbryURI';
|
||||||
import classnames from 'classnames';
|
import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
|
||||||
import CommentReactions from 'component/commentReactions';
|
|
||||||
import CommentsReplies from 'component/commentsReplies';
|
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import classnames from 'classnames';
|
||||||
import CommentCreate from 'component/commentCreate';
|
import CommentCreate from 'component/commentCreate';
|
||||||
import CommentMenuList from 'component/commentMenuList';
|
import CommentMenuList from 'component/commentMenuList';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import CommentReactions from 'component/commentReactions';
|
||||||
|
import CommentsReplies from 'component/commentsReplies';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import DateTime from 'component/dateTime';
|
||||||
|
import Expandable from 'component/expandable';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import { parseSticker } from 'util/comments';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
|
||||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||||
|
|
||||||
type Props = {
|
|
||||||
clearPlayingUri: () => void,
|
|
||||||
uri: string,
|
|
||||||
claim: StreamClaim,
|
|
||||||
author: ?string, // LBRY Channel Name, e.g. @channel
|
|
||||||
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
|
||||||
commentId: string, // sha256 digest identifying the comment
|
|
||||||
message: string, // comment body
|
|
||||||
timePosted: number, // Comment timestamp
|
|
||||||
channelIsBlocked: boolean, // if the channel is blacklisted in the app
|
|
||||||
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
|
||||||
commentIsMine: boolean, // if this comment was signed by an owned channel
|
|
||||||
updateComment: (string, string) => void,
|
|
||||||
fetchReplies: (string, string, number, number, number) => void,
|
|
||||||
totalReplyPages: number,
|
|
||||||
commentModBlock: (string) => void,
|
|
||||||
linkedCommentId?: string,
|
|
||||||
linkedCommentAncestors: { [string]: Array<string> },
|
|
||||||
myChannels: ?Array<ChannelClaim>,
|
|
||||||
commentingEnabled: boolean,
|
|
||||||
doToast: ({ message: string }) => void,
|
|
||||||
isTopLevel?: boolean,
|
|
||||||
threadDepth: number,
|
|
||||||
hideActions?: boolean,
|
|
||||||
isPinned: boolean,
|
|
||||||
othersReacts: ?{
|
|
||||||
like: number,
|
|
||||||
dislike: number,
|
|
||||||
},
|
|
||||||
commentIdentityChannel: any,
|
|
||||||
activeChannelClaim: ?ChannelClaim,
|
|
||||||
playingUri: ?PlayingUri,
|
|
||||||
stakedLevel: number,
|
|
||||||
supportAmount: number,
|
|
||||||
numDirectReplies: number,
|
|
||||||
isModerator: boolean,
|
|
||||||
isGlobalMod: boolean,
|
|
||||||
isFiat: boolean,
|
|
||||||
supportDisabled: boolean,
|
|
||||||
setQuickReply: (any) => void,
|
|
||||||
quickReply: any,
|
|
||||||
};
|
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
|
||||||
function Comment(props: Props) {
|
type Props = {
|
||||||
|
channelIsBlocked: boolean, // if the channel is blacklisted in the app
|
||||||
|
claim: StreamClaim,
|
||||||
|
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
||||||
|
comment: Comment,
|
||||||
|
commentIdentityChannel: any,
|
||||||
|
hideActions?: boolean,
|
||||||
|
isTopLevel?: boolean,
|
||||||
|
linkedCommentAncestors: { [string]: Array<string> },
|
||||||
|
linkedCommentId?: string,
|
||||||
|
myChannels: ?Array<ChannelClaim>,
|
||||||
|
othersReacts: ?{ like: number, dislike: number },
|
||||||
|
playingUri: ?PlayingUri,
|
||||||
|
quickReply: any,
|
||||||
|
stakedLevel: number,
|
||||||
|
supportDisabled: boolean,
|
||||||
|
threadDepth: number,
|
||||||
|
totalReplyPages: number,
|
||||||
|
uri: string,
|
||||||
|
userCanComment: boolean,
|
||||||
|
clearPlayingUri: () => void,
|
||||||
|
commentModBlock: (string) => void,
|
||||||
|
fetchReplies: (number, number, string) => void,
|
||||||
|
setQuickReply: (any) => void,
|
||||||
|
updateComment: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function CommentView(props: Props) {
|
||||||
const {
|
const {
|
||||||
clearPlayingUri,
|
|
||||||
claim,
|
|
||||||
uri,
|
|
||||||
author,
|
|
||||||
authorUri,
|
|
||||||
timePosted,
|
|
||||||
message,
|
|
||||||
channelIsBlocked,
|
channelIsBlocked,
|
||||||
commentIsMine,
|
claim,
|
||||||
commentId,
|
comment,
|
||||||
updateComment,
|
|
||||||
fetchReplies,
|
|
||||||
totalReplyPages,
|
|
||||||
linkedCommentId,
|
|
||||||
linkedCommentAncestors,
|
|
||||||
commentingEnabled,
|
|
||||||
myChannels,
|
|
||||||
doToast,
|
|
||||||
isTopLevel,
|
|
||||||
threadDepth,
|
|
||||||
hideActions,
|
hideActions,
|
||||||
isPinned,
|
isTopLevel,
|
||||||
|
linkedCommentAncestors,
|
||||||
|
linkedCommentId,
|
||||||
|
myChannels,
|
||||||
othersReacts,
|
othersReacts,
|
||||||
playingUri,
|
playingUri,
|
||||||
stakedLevel,
|
|
||||||
supportAmount,
|
|
||||||
numDirectReplies,
|
|
||||||
isModerator,
|
|
||||||
isGlobalMod,
|
|
||||||
isFiat,
|
|
||||||
supportDisabled,
|
|
||||||
setQuickReply,
|
|
||||||
quickReply,
|
quickReply,
|
||||||
|
stakedLevel,
|
||||||
|
supportDisabled,
|
||||||
|
threadDepth,
|
||||||
|
totalReplyPages,
|
||||||
|
uri,
|
||||||
|
userCanComment,
|
||||||
|
clearPlayingUri,
|
||||||
|
fetchReplies,
|
||||||
|
setQuickReply,
|
||||||
|
updateComment,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
push,
|
channel_id: authorId,
|
||||||
|
channel_url: authorUri,
|
||||||
|
comment_id: commentId,
|
||||||
|
comment: message,
|
||||||
|
is_fiat: isFiat,
|
||||||
|
is_global_mod: isGlobalMod,
|
||||||
|
is_moderator: isModerator,
|
||||||
|
is_pinned: isPinned,
|
||||||
|
replies: numDirectReplies,
|
||||||
|
support_amount: supportAmount,
|
||||||
|
} = comment;
|
||||||
|
const timePosted = comment.timestamp * 1000;
|
||||||
|
const commentIsMine = authorId && myChannels && myChannels.some(({ claim_id }) => claim_id === authorId);
|
||||||
|
|
||||||
|
const {
|
||||||
replace,
|
replace,
|
||||||
location: { pathname, search },
|
location: { pathname, search },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
@ -129,79 +109,22 @@ function Comment(props: Props) {
|
||||||
const [isReplying, setReplying] = React.useState(false);
|
const [isReplying, setReplying] = React.useState(false);
|
||||||
const [isEditing, setEditing] = useState(false);
|
const [isEditing, setEditing] = useState(false);
|
||||||
const [editedMessage, setCommentValue] = useState(message);
|
const [editedMessage, setCommentValue] = useState(message);
|
||||||
const [charCount, setCharCount] = useState(editedMessage.length);
|
|
||||||
const [showReplies, setShowReplies] = useState(showRepliesOnMount);
|
const [showReplies, setShowReplies] = useState(showRepliesOnMount);
|
||||||
const [page, setPage] = useState(showRepliesOnMount ? 1 : 0);
|
const [page, setPage] = useState(showRepliesOnMount ? 1 : 0);
|
||||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
|
||||||
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
||||||
const hasChannels = myChannels && myChannels.length > 0;
|
|
||||||
const likesCount = (othersReacts && othersReacts.like) || 0;
|
const likesCount = (othersReacts && othersReacts.like) || 0;
|
||||||
const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
|
const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
|
||||||
const totalLikesAndDislikes = likesCount + dislikesCount;
|
const totalLikesAndDislikes = likesCount + dislikesCount;
|
||||||
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const commentByContentOwner = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
const stickerFromMessage = parseSticker(message);
|
const stickerFromMessage = parseSticker(message);
|
||||||
|
|
||||||
let channelOwnerOfContent;
|
let contentOwnerChannel;
|
||||||
try {
|
try {
|
||||||
const { channelName } = parseURI(uri);
|
({ channelName: contentOwnerChannel } = parseURI(uri));
|
||||||
if (channelName) {
|
|
||||||
channelOwnerOfContent = channelName;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing) {
|
|
||||||
setCharCount(editedMessage.length);
|
|
||||||
|
|
||||||
// a user will try and press the escape key to cancel editing their comment
|
|
||||||
const handleEscape = (event) => {
|
|
||||||
if (event.keyCode === KEYCODES.ESCAPE) {
|
|
||||||
setEditing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleEscape);
|
|
||||||
|
|
||||||
// removes the listener so it doesn't cause problems elsewhere in the app
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleEscape);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [author, authorUri, editedMessage, isEditing, setEditing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (page > 0) {
|
|
||||||
fetchReplies(uri, commentId, page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST);
|
|
||||||
}
|
|
||||||
}, [page, uri, commentId, fetchReplies]);
|
|
||||||
|
|
||||||
function handleEditMessageChanged(event) {
|
|
||||||
setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditComment() {
|
|
||||||
if (playingUri && playingUri.source === 'comment') {
|
|
||||||
clearPlayingUri();
|
|
||||||
}
|
|
||||||
setEditing(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
updateComment(commentId, editedMessage);
|
|
||||||
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
|
|
||||||
setEditing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommentReply() {
|
|
||||||
if (!hasChannels) {
|
|
||||||
push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`);
|
|
||||||
doToast({ message: __('A channel is required to comment on %SITE_NAME%', { SITE_NAME }) });
|
|
||||||
} else {
|
|
||||||
setReplying(!isReplying);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTimeClick() {
|
function handleTimeClick() {
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
urlParams.delete('lc');
|
urlParams.delete('lc');
|
||||||
|
@ -221,10 +144,27 @@ function Comment(props: Props) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (page > 0) fetchReplies(page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST);
|
||||||
|
}, [commentId, fetchReplies, page, uri]);
|
||||||
|
|
||||||
|
const commentBadge = (label: string, className: string, icon: string) => (
|
||||||
|
<Tooltip label={label}>
|
||||||
|
<span className={`comment__badge ${className}`}>
|
||||||
|
<Icon icon={icon} size={20} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MarkdownWrapper =
|
||||||
|
editedMessage.length >= LENGTH_TO_COLLAPSE
|
||||||
|
? ({ children }) => <Expandable>{children}</Expandable>
|
||||||
|
: ({ children }) => children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classnames('comment', {
|
className={classnames('comment', {
|
||||||
'comment--top-level': isTopLevel,
|
'comment--topLevel': isTopLevel,
|
||||||
'comment--reply': !isTopLevel,
|
'comment--reply': !isTopLevel,
|
||||||
'comment--superchat': supportAmount > 0,
|
'comment--superchat': supportAmount > 0,
|
||||||
})}
|
})}
|
||||||
|
@ -233,48 +173,25 @@ function Comment(props: Props) {
|
||||||
<div
|
<div
|
||||||
ref={isLinkedComment ? linkedCommentRef : undefined}
|
ref={isLinkedComment ? linkedCommentRef : undefined}
|
||||||
className={classnames('comment__content', {
|
className={classnames('comment__content', {
|
||||||
[COMMENT_HIGHLIGHTED]: isLinkedComment,
|
'comment__content--highlighted': isLinkedComment,
|
||||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
'comment__content--slimed': slimedToDeath && !displayDeadComment,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="comment__thumbnail-wrapper">
|
<div className="commentThumbnail__wrapper">
|
||||||
{authorUri ? (
|
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="commentAuthor__thumbnail" />
|
||||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" />
|
|
||||||
) : (
|
|
||||||
<ChannelThumbnail xsmall className="comment__author-thumbnail" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="comment__body-container">
|
<div className="commentBody__container">
|
||||||
<div className="comment__meta">
|
<div className="comment__meta">
|
||||||
<div className="comment__meta-information">
|
<div className="commentMeta__information">
|
||||||
{isGlobalMod && (
|
{isModerator && commentBadge(__('Moderator'), 'commentBadge__mod', ICONS.BADGE_MOD)}
|
||||||
<Tooltip label={__('Admin')}>
|
{isGlobalMod && commentBadge(__('Admin'), 'commentBadge__globalMod', ICONS.BADGE_MOD)}
|
||||||
<span className="comment__badge comment__badge--global-mod">
|
|
||||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isModerator && (
|
<UriIndicator
|
||||||
<Tooltip label={__('Moderator')}>
|
uri={authorUri}
|
||||||
<span className="comment__badge comment__badge--mod">
|
className={classnames('comment__author', { 'comment__author--creator': commentByContentOwner })}
|
||||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
link
|
||||||
</span>
|
/>
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!author ? (
|
|
||||||
<span className="comment__author">{__('Anonymous')}</span>
|
|
||||||
) : (
|
|
||||||
<UriIndicator
|
|
||||||
className={classnames('comment__author', {
|
|
||||||
'comment__author--creator': commentByOwnerOfContent,
|
|
||||||
})}
|
|
||||||
link
|
|
||||||
uri={authorUri}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
className="comment__time"
|
className="comment__time"
|
||||||
onClick={handleTimeClick}
|
onClick={handleTimeClick}
|
||||||
|
@ -286,8 +203,8 @@ function Comment(props: Props) {
|
||||||
{isPinned && (
|
{isPinned && (
|
||||||
<span className="comment__pin">
|
<span className="comment__pin">
|
||||||
<Icon icon={ICONS.PIN} size={14} />
|
<Icon icon={ICONS.PIN} size={14} />
|
||||||
{channelOwnerOfContent
|
{contentOwnerChannel
|
||||||
? __('Pinned by @%channel%', { channel: channelOwnerOfContent })
|
? __('Pinned by @%channel%', { channel: contentOwnerChannel })
|
||||||
: __('Pinned by creator')}
|
: __('Pinned by creator')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -304,63 +221,57 @@ function Comment(props: Props) {
|
||||||
commentId={commentId}
|
commentId={commentId}
|
||||||
authorUri={authorUri}
|
authorUri={authorUri}
|
||||||
commentIsMine={commentIsMine}
|
commentIsMine={commentIsMine}
|
||||||
handleEditComment={handleEditComment}
|
handleEditComment={() => {
|
||||||
|
if (playingUri && playingUri.source === 'comment') clearPlayingUri();
|
||||||
|
setEditing(true);
|
||||||
|
}}
|
||||||
supportAmount={supportAmount}
|
supportAmount={supportAmount}
|
||||||
setQuickReply={setQuickReply}
|
setQuickReply={setQuickReply}
|
||||||
|
disableEdit={Boolean(stickerFromMessage)}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="comment__body">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<Form onSubmit={handleSubmit}>
|
<CommentCreate
|
||||||
<FormField
|
isEdit
|
||||||
className="comment__edit-input"
|
editedMessage={message}
|
||||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
onDoneEditing={(editedMessage) => {
|
||||||
name="editing_comment"
|
if (editedMessage) {
|
||||||
value={editedMessage}
|
updateComment(editedMessage);
|
||||||
charCount={charCount}
|
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
|
||||||
onChange={handleEditMessageChanged}
|
setCommentValue(editedMessage);
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
}
|
||||||
/>
|
setEditing(false);
|
||||||
<div className="section__actions section__actions--no-margin">
|
}}
|
||||||
<Button
|
supportDisabled
|
||||||
button="primary"
|
/>
|
||||||
type="submit"
|
|
||||||
label={__('Done')}
|
|
||||||
requiresAuth={IS_WEB}
|
|
||||||
disabled={message === editedMessage}
|
|
||||||
/>
|
|
||||||
<Button button="link" label={__('Cancel')} onClick={() => setEditing(false)} />
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="comment__message">
|
<div
|
||||||
|
className={classnames('comment__message', {
|
||||||
|
'comment__message--dead': slimedToDeath && !displayDeadComment,
|
||||||
|
'comment__message--sticker': stickerFromMessage,
|
||||||
|
})}
|
||||||
|
onClick={() => slimedToDeath && setDisplayDeadComment(true)}
|
||||||
|
>
|
||||||
{slimedToDeath && !displayDeadComment ? (
|
{slimedToDeath && !displayDeadComment ? (
|
||||||
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
<>
|
||||||
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
{__('This comment was slimed to death.')}
|
||||||
</div>
|
<Icon icon={ICONS.SLIME_ACTIVE} />
|
||||||
|
</>
|
||||||
) : stickerFromMessage ? (
|
) : stickerFromMessage ? (
|
||||||
<div className="sticker__comment">
|
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
) : (
|
||||||
</div>
|
<MarkdownWrapper>
|
||||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
|
||||||
<Expandable>
|
|
||||||
<MarkdownPreview
|
<MarkdownPreview
|
||||||
content={message}
|
content={message}
|
||||||
promptLinks
|
|
||||||
parentCommentId={commentId}
|
parentCommentId={commentId}
|
||||||
stakedLevel={stakedLevel}
|
stakedLevel={stakedLevel}
|
||||||
|
promptLinks
|
||||||
/>
|
/>
|
||||||
</Expandable>
|
</MarkdownWrapper>
|
||||||
) : (
|
|
||||||
<MarkdownPreview
|
|
||||||
content={message}
|
|
||||||
promptLinks
|
|
||||||
parentCommentId={commentId}
|
|
||||||
stakedLevel={stakedLevel}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -368,10 +279,10 @@ function Comment(props: Props) {
|
||||||
<div className="comment__actions">
|
<div className="comment__actions">
|
||||||
{threadDepth !== 0 && (
|
{threadDepth !== 0 && (
|
||||||
<Button
|
<Button
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth
|
||||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
label={userCanComment ? __('Reply') : __('Log in to reply')}
|
||||||
className="comment__action"
|
className="comment__action"
|
||||||
onClick={handleCommentReply}
|
onClick={() => setReplying(!isReplying)}
|
||||||
icon={ICONS.REPLY}
|
icon={ICONS.REPLY}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -379,36 +290,33 @@ function Comment(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{numDirectReplies > 0 && !showReplies && (
|
{numDirectReplies > 0 &&
|
||||||
<div className="comment__actions">
|
(!showReplies ? (
|
||||||
<Button
|
<div className="comment__actions">
|
||||||
label={
|
<Button
|
||||||
numDirectReplies < 2
|
label={
|
||||||
? __('Show reply')
|
numDirectReplies < 2
|
||||||
: __('Show %count% replies', { count: numDirectReplies })
|
? __('Show reply')
|
||||||
}
|
: __('Show %count% replies', { count: numDirectReplies })
|
||||||
button="link"
|
|
||||||
onClick={() => {
|
|
||||||
setShowReplies(true);
|
|
||||||
if (page === 0) {
|
|
||||||
setPage(1);
|
|
||||||
}
|
}
|
||||||
}}
|
button="link"
|
||||||
icon={ICONS.DOWN}
|
onClick={() => {
|
||||||
/>
|
setShowReplies(true);
|
||||||
</div>
|
if (page === 0) setPage(1);
|
||||||
)}
|
}}
|
||||||
|
icon={ICONS.DOWN}
|
||||||
{numDirectReplies > 0 && showReplies && (
|
/>
|
||||||
<div className="comment__actions">
|
</div>
|
||||||
<Button
|
) : (
|
||||||
label={__('Hide replies')}
|
<div className="comment__actions">
|
||||||
button="link"
|
<Button
|
||||||
onClick={() => setShowReplies(false)}
|
label={__('Hide replies')}
|
||||||
icon={ICONS.UP}
|
button="link"
|
||||||
/>
|
onClick={() => setShowReplies(false)}
|
||||||
</div>
|
icon={ICONS.UP}
|
||||||
)}
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{isReplying && (
|
{isReplying && (
|
||||||
<CommentCreate
|
<CommentCreate
|
||||||
|
@ -419,9 +327,7 @@ function Comment(props: Props) {
|
||||||
setShowReplies(true);
|
setShowReplies(true);
|
||||||
setReplying(false);
|
setReplying(false);
|
||||||
}}
|
}}
|
||||||
onCancelReplying={() => {
|
onCancelReplying={() => setReplying(false)}
|
||||||
setReplying(false);
|
|
||||||
}}
|
|
||||||
supportDisabled={supportDisabled}
|
supportDisabled={supportDisabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -446,4 +352,4 @@ function Comment(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Comment;
|
export default CommentView;
|
||||||
|
|
|
@ -21,7 +21,7 @@ const select = (state, props) => ({
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
isFetchingChannels: selectFetchingMyChannels(state),
|
isFetchingChannels: selectFetchingMyChannels(state),
|
||||||
settingsByChannelId: selectSettingsByChannelId(state),
|
settingsByChannelId: selectSettingsByChannelId(state),
|
||||||
supportDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state),
|
supportDisabled: props.supportDisabled || makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
|
|
|
@ -45,7 +45,9 @@ type Props = {
|
||||||
channels: ?Array<ChannelClaim>,
|
channels: ?Array<ChannelClaim>,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
|
editedMessage?: string,
|
||||||
embed?: boolean,
|
embed?: boolean,
|
||||||
|
isEdit?: boolean,
|
||||||
isFetchingChannels: boolean,
|
isFetchingChannels: boolean,
|
||||||
isNested: boolean,
|
isNested: boolean,
|
||||||
isReply: boolean,
|
isReply: boolean,
|
||||||
|
@ -60,6 +62,7 @@ type Props = {
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
fetchComment: (commentId: string) => Promise<any>,
|
fetchComment: (commentId: string) => Promise<any>,
|
||||||
onCancelReplying?: () => void,
|
onCancelReplying?: () => void,
|
||||||
|
onDoneEditing?: (editedMessage?: string) => void,
|
||||||
onDoneReplying?: () => void,
|
onDoneReplying?: () => void,
|
||||||
sendCashTip: (TipParams, UserParams, string, ?string, (any) => void) => string,
|
sendCashTip: (TipParams, UserParams, string, ?string, (any) => void) => string,
|
||||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||||
|
@ -74,7 +77,9 @@ export function CommentCreate(props: Props) {
|
||||||
channels,
|
channels,
|
||||||
claim,
|
claim,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
|
editedMessage,
|
||||||
embed,
|
embed,
|
||||||
|
isEdit,
|
||||||
isFetchingChannels,
|
isFetchingChannels,
|
||||||
isNested,
|
isNested,
|
||||||
isReply,
|
isReply,
|
||||||
|
@ -91,6 +96,7 @@ export function CommentCreate(props: Props) {
|
||||||
onCancelReplying,
|
onCancelReplying,
|
||||||
onDoneReplying,
|
onDoneReplying,
|
||||||
sendCashTip,
|
sendCashTip,
|
||||||
|
onDoneEditing,
|
||||||
sendTip,
|
sendTip,
|
||||||
setQuickReply,
|
setQuickReply,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -114,7 +120,7 @@ export function CommentCreate(props: Props) {
|
||||||
const [selectedSticker, setSelectedSticker] = React.useState();
|
const [selectedSticker, setSelectedSticker] = React.useState();
|
||||||
const [tipAmount, setTipAmount] = React.useState(1);
|
const [tipAmount, setTipAmount] = React.useState(1);
|
||||||
const [convertedAmount, setConvertedAmount] = React.useState();
|
const [convertedAmount, setConvertedAmount] = React.useState();
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState(editedMessage || '');
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const [stickerSelector, setStickerSelector] = React.useState();
|
const [stickerSelector, setStickerSelector] = React.useState();
|
||||||
const [activeTab, setActiveTab] = React.useState('');
|
const [activeTab, setActiveTab] = React.useState('');
|
||||||
|
@ -474,16 +480,18 @@ export function CommentCreate(props: Props) {
|
||||||
<FormField
|
<FormField
|
||||||
disabled={isFetchingChannels}
|
disabled={isFetchingChannels}
|
||||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={(isReply && 'content_reply') || (isEdit && 'editing_comment') || 'content_description'}
|
||||||
ref={formFieldRef}
|
ref={formFieldRef}
|
||||||
className={isReply ? 'content_reply' : 'content_comment'}
|
className={(isReply && 'content_reply') || (isEdit && 'commentEdit__input') || 'content_comment'}
|
||||||
label={
|
label={
|
||||||
<span className="commentCreate__labelWrapper">
|
!isEdit && (
|
||||||
{!livestream && (
|
<span className="commentCreate__labelWrapper">
|
||||||
<div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div>
|
{!livestream && (
|
||||||
)}
|
<div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div>
|
||||||
<SelectChannel tiny />
|
)}
|
||||||
</span>
|
<SelectChannel tiny />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
quickActionLabel={
|
quickActionLabel={
|
||||||
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
||||||
|
@ -577,16 +585,17 @@ export function CommentCreate(props: Props) {
|
||||||
disabled={disabled || stickerSelector}
|
disabled={disabled || stickerSelector}
|
||||||
type="submit"
|
type="submit"
|
||||||
label={
|
label={
|
||||||
isReply
|
(isEdit && __('Done')) ||
|
||||||
? isSubmitting
|
(isSubmitting
|
||||||
? __('Replying...')
|
? (isReply && __('Replying...')) || __('Commenting...')
|
||||||
: __('Reply')
|
: (isReply && __('Reply')) || __('Comment --[button to submit something]--'))
|
||||||
: isSubmitting
|
|
||||||
? __('Commenting...')
|
|
||||||
: __('Comment --[button to submit something]--')
|
|
||||||
}
|
}
|
||||||
requiresAuth
|
requiresAuth
|
||||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
onClick={() =>
|
||||||
|
activeChannelClaim &&
|
||||||
|
commentValue.length &&
|
||||||
|
(isEdit && editedMessage && onDoneEditing ? onDoneEditing() : handleCreateComment())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
@ -629,7 +638,8 @@ export function CommentCreate(props: Props) {
|
||||||
isReviewingSupportComment ||
|
isReviewingSupportComment ||
|
||||||
stickerSelector ||
|
stickerSelector ||
|
||||||
isReviewingStickerComment ||
|
isReviewingStickerComment ||
|
||||||
(isReply && !minTip)) && (
|
(isReply && !minTip) ||
|
||||||
|
isEdit) && (
|
||||||
<Button
|
<Button
|
||||||
disabled={isSupportComment && isSubmitting}
|
disabled={isSupportComment && isSubmitting}
|
||||||
button="link"
|
button="link"
|
||||||
|
@ -648,6 +658,8 @@ export function CommentCreate(props: Props) {
|
||||||
setStickerSelector(false);
|
setStickerSelector(false);
|
||||||
} else if (isReply && !minTip && onCancelReplying) {
|
} else if (isReply && !minTip && onCancelReplying) {
|
||||||
onCancelReplying();
|
onCancelReplying();
|
||||||
|
} else if (isEdit && onDoneEditing) {
|
||||||
|
onDoneEditing();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doResolveUris } from 'redux/actions/claims';
|
|
||||||
import {
|
|
||||||
makeSelectClaimForUri,
|
|
||||||
makeSelectClaimIsMine,
|
|
||||||
selectFetchingMyChannels,
|
|
||||||
selectMyChannelClaims,
|
|
||||||
} from 'redux/selectors/claims';
|
|
||||||
import {
|
import {
|
||||||
selectTopLevelCommentsForUri,
|
selectTopLevelCommentsForUri,
|
||||||
makeSelectTopLevelTotalPagesForUri,
|
makeSelectTopLevelTotalPagesForUri,
|
||||||
|
@ -20,6 +13,8 @@ import {
|
||||||
selectPinnedCommentsForUri,
|
selectPinnedCommentsForUri,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||||
|
import { doResolveUris } from 'redux/actions/claims';
|
||||||
|
import { makeSelectClaimForUri, makeSelectClaimIsMine, selectFetchingMyChannels } from 'redux/selectors/claims';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
|
||||||
|
@ -33,32 +28,31 @@ const select = (state, props) => {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
topLevelComments,
|
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||||
resolvedComments,
|
|
||||||
myChannels: selectMyChannelClaims(state),
|
|
||||||
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
||||||
pinnedComments: selectPinnedCommentsForUri(state, props.uri),
|
|
||||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
|
||||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
isFetchingComments: selectIsFetchingComments(state),
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
isFetchingCommentsById: selectIsFetchingCommentsById(state),
|
isFetchingCommentsById: selectIsFetchingCommentsById(state),
|
||||||
isFetchingReacts: selectIsFetchingReacts(state),
|
isFetchingReacts: selectIsFetchingReacts(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
|
||||||
settingsByChannelId: selectSettingsByChannelId(state),
|
|
||||||
myReactsByCommentId: selectMyReacts(state),
|
myReactsByCommentId: selectMyReacts(state),
|
||||||
othersReactsById: selectOthersReacts(state),
|
othersReactsById: selectOthersReacts(state),
|
||||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
pinnedComments: selectPinnedCommentsForUri(state, props.uri),
|
||||||
|
resolvedComments,
|
||||||
|
settingsByChannelId: selectSettingsByChannelId(state),
|
||||||
|
topLevelComments,
|
||||||
|
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
||||||
|
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
||||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||||
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
fetchTopLevelComments: (page, pageSize, sortBy) => dispatch(doCommentList(ownProps.uri, '', page, pageSize, sortBy)),
|
||||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
resetComments: () => ownProps.claim && dispatch(doCommentReset(ownProps.claim.claimId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import 'scss/component/_comments-list.scss';
|
||||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
|
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
|
@ -18,67 +20,57 @@ import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
|
||||||
function scaleToDevicePixelRatio(value) {
|
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
|
||||||
if (devicePixelRatio < 1.0) {
|
|
||||||
return Math.ceil(value / devicePixelRatio);
|
|
||||||
}
|
|
||||||
return Math.ceil(value * devicePixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
activeChannelId: ?string,
|
||||||
allCommentIds: any,
|
allCommentIds: any,
|
||||||
pinnedComments: Array<Comment>,
|
|
||||||
topLevelComments: Array<Comment>,
|
|
||||||
resolvedComments: Array<Comment>,
|
|
||||||
topLevelTotalPages: number,
|
|
||||||
uri: string,
|
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
commentsAreExpanded?: boolean,
|
||||||
|
fetchingChannels: boolean,
|
||||||
isFetchingComments: boolean,
|
isFetchingComments: boolean,
|
||||||
isFetchingCommentsById: boolean,
|
isFetchingCommentsById: boolean,
|
||||||
isFetchingReacts: boolean,
|
isFetchingReacts: boolean,
|
||||||
linkedCommentId?: string,
|
linkedCommentId?: string,
|
||||||
totalComments: number,
|
|
||||||
fetchingChannels: boolean,
|
|
||||||
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
|
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
|
||||||
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||||
activeChannelId: ?string,
|
pinnedComments: Array<Comment>,
|
||||||
|
resolvedComments: Array<Comment>,
|
||||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||||
commentsAreExpanded?: boolean,
|
topLevelComments: Array<Comment>,
|
||||||
fetchReacts: (Array<string>) => Promise<any>,
|
topLevelTotalPages: number,
|
||||||
|
totalComments: number,
|
||||||
|
uri: string,
|
||||||
doResolveUris: (Array<string>) => void,
|
doResolveUris: (Array<string>) => void,
|
||||||
fetchTopLevelComments: (string, number, number, number) => void,
|
|
||||||
fetchComment: (string) => void,
|
fetchComment: (string) => void,
|
||||||
resetComments: (string) => void,
|
fetchReacts: (Array<string>) => Promise<any>,
|
||||||
|
fetchTopLevelComments: (number, number, string) => void,
|
||||||
|
resetComments: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentList(props: Props) {
|
function CommentsList(props: Props) {
|
||||||
const {
|
const {
|
||||||
|
activeChannelId,
|
||||||
allCommentIds,
|
allCommentIds,
|
||||||
uri,
|
|
||||||
pinnedComments,
|
|
||||||
topLevelComments,
|
|
||||||
resolvedComments,
|
|
||||||
topLevelTotalPages,
|
|
||||||
claim,
|
claim,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
myChannels,
|
commentsAreExpanded,
|
||||||
|
doResolveUris,
|
||||||
|
fetchingChannels,
|
||||||
|
fetchReacts,
|
||||||
isFetchingComments,
|
isFetchingComments,
|
||||||
isFetchingReacts,
|
isFetchingReacts,
|
||||||
linkedCommentId,
|
linkedCommentId,
|
||||||
totalComments,
|
|
||||||
fetchingChannels,
|
|
||||||
myReactsByCommentId,
|
myReactsByCommentId,
|
||||||
othersReactsById,
|
othersReactsById,
|
||||||
activeChannelId,
|
pinnedComments,
|
||||||
|
resolvedComments,
|
||||||
settingsByChannelId,
|
settingsByChannelId,
|
||||||
commentsAreExpanded,
|
topLevelComments,
|
||||||
fetchReacts,
|
topLevelTotalPages,
|
||||||
doResolveUris,
|
totalComments,
|
||||||
fetchTopLevelComments,
|
uri,
|
||||||
fetchComment,
|
fetchComment,
|
||||||
|
fetchTopLevelComments,
|
||||||
resetComments,
|
resetComments,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -92,6 +84,7 @@ function CommentList(props: Props) {
|
||||||
const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
|
const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
|
||||||
const hasDefaultExpansion = commentsAreExpanded || desktopView;
|
const hasDefaultExpansion = commentsAreExpanded || desktopView;
|
||||||
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
|
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
|
||||||
|
|
||||||
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||||
const channelId = getChannelIdFromClaim(claim);
|
const channelId = getChannelIdFromClaim(claim);
|
||||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||||
|
@ -106,7 +99,7 @@ function CommentList(props: Props) {
|
||||||
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
function changeSort(newSort) {
|
function changeSort(newSort: string) {
|
||||||
if (sort !== newSort) {
|
if (sort !== newSort) {
|
||||||
setSort(newSort);
|
setSort(newSort);
|
||||||
setPage(0); // Invalidate existing comments
|
setPage(0); // Invalidate existing comments
|
||||||
|
@ -116,9 +109,7 @@ function CommentList(props: Props) {
|
||||||
// Reset comments
|
// Reset comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
if (claim) {
|
if (claim) resetComments();
|
||||||
resetComments(claim.claim_id);
|
|
||||||
}
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -131,7 +122,7 @@ function CommentList(props: Props) {
|
||||||
fetchComment(linkedCommentId);
|
fetchComment(linkedCommentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTopLevelComments(uri, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
fetchTopLevelComments(page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
||||||
}
|
}
|
||||||
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]);
|
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]);
|
||||||
|
|
||||||
|
@ -237,39 +228,25 @@ function CommentList(props: Props) {
|
||||||
if (!topLevelComments || alreadyResolved) return;
|
if (!topLevelComments || alreadyResolved) return;
|
||||||
|
|
||||||
const urisToResolve = [];
|
const urisToResolve = [];
|
||||||
topLevelComments.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
|
topLevelComments.forEach(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
|
||||||
|
|
||||||
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
||||||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||||
|
|
||||||
const getCommentElems = (comments) =>
|
const getCommentElems = (comments: Array<Comment>) =>
|
||||||
comments.map((comment) => (
|
comments.map((comment: Comment) => (
|
||||||
<CommentView
|
<CommentView
|
||||||
isTopLevel
|
|
||||||
threadDepth={3}
|
|
||||||
key={comment.comment_id}
|
|
||||||
uri={uri}
|
|
||||||
authorUri={comment.channel_url}
|
|
||||||
author={comment.channel_name}
|
|
||||||
claimId={comment.claim_id}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
timePosted={comment.timestamp * 1000}
|
|
||||||
claimIsMine={claimIsMine}
|
claimIsMine={claimIsMine}
|
||||||
commentIsMine={
|
comment={comment}
|
||||||
comment.channel_id && myChannels && myChannels.some(({ claim_id }) => claim_id === comment.channel_id)
|
isTopLevel
|
||||||
}
|
key={comment.comment_id}
|
||||||
linkedCommentId={linkedCommentId}
|
linkedCommentId={linkedCommentId}
|
||||||
isPinned={comment.is_pinned}
|
threadDepth={3}
|
||||||
supportAmount={comment.support_amount}
|
uri={uri}
|
||||||
numDirectReplies={comment.replies}
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
isFiat={comment.is_fiat}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
const sortButton = (label, icon, sortOption) => (
|
const sortButton = (label: string, icon: string, sortOption: string) => (
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
label={label}
|
label={label}
|
||||||
|
@ -311,8 +288,7 @@ function CommentList(props: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
className={classnames({
|
className={classnames('comments', {
|
||||||
comments: desktopView || expandedComments,
|
|
||||||
'comments--contracted': !desktopView && !expandedComments,
|
'comments--contracted': !desktopView && !expandedComments,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -352,4 +328,4 @@ function CommentList(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommentList;
|
export default CommentsList;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doResolveUris } from 'redux/actions/claims';
|
import { doResolveUris } from 'redux/actions/claims';
|
||||||
import { makeSelectClaimIsMine, selectMyChannelClaims, makeSelectClaimForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments';
|
import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import CommentsReplies from './view';
|
import CommentsReplies from './view';
|
||||||
|
@ -13,12 +13,11 @@ const select = (state, props) => {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetchedReplies,
|
|
||||||
resolvedReplies,
|
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
userCanComment: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
fetchedReplies,
|
||||||
myChannels: selectMyChannelClaims(state),
|
|
||||||
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
||||||
|
resolvedReplies,
|
||||||
|
userCanComment: selectUserVerifiedEmail(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import 'scss/component/_comments.scss';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Comment from 'component/comment';
|
import Comment from 'component/comment';
|
||||||
|
@ -6,44 +7,43 @@ import React from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fetchedReplies: Array<Comment>,
|
|
||||||
resolvedReplies: Array<Comment>,
|
|
||||||
uri: string,
|
|
||||||
parentId: string,
|
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
fetchedReplies: Array<Comment>,
|
||||||
linkedCommentId?: string,
|
|
||||||
userCanComment: boolean,
|
|
||||||
threadDepth: number,
|
|
||||||
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
|
|
||||||
isFetchingByParentId: { [string]: boolean },
|
|
||||||
hasMore: boolean,
|
hasMore: boolean,
|
||||||
|
isFetchingByParentId: { [string]: boolean },
|
||||||
|
linkedCommentId?: string,
|
||||||
|
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
|
||||||
|
parentId: string,
|
||||||
|
resolvedReplies: Array<Comment>,
|
||||||
supportDisabled: boolean,
|
supportDisabled: boolean,
|
||||||
|
threadDepth: number,
|
||||||
|
uri: string,
|
||||||
|
userCanComment: boolean,
|
||||||
doResolveUris: (Array<string>) => void,
|
doResolveUris: (Array<string>) => void,
|
||||||
onShowMore?: () => void,
|
onShowMore?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentsReplies(props: Props) {
|
function CommentsReplies(props: Props) {
|
||||||
const {
|
const {
|
||||||
uri,
|
|
||||||
parentId,
|
|
||||||
fetchedReplies,
|
|
||||||
resolvedReplies,
|
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
myChannels,
|
fetchedReplies,
|
||||||
linkedCommentId,
|
|
||||||
userCanComment,
|
|
||||||
threadDepth,
|
|
||||||
numDirectReplies,
|
|
||||||
isFetchingByParentId,
|
|
||||||
hasMore,
|
hasMore,
|
||||||
|
isFetchingByParentId,
|
||||||
|
linkedCommentId,
|
||||||
|
numDirectReplies,
|
||||||
|
parentId,
|
||||||
|
resolvedReplies,
|
||||||
supportDisabled,
|
supportDisabled,
|
||||||
|
threadDepth,
|
||||||
|
uri,
|
||||||
|
userCanComment,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
onShowMore,
|
onShowMore,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isExpanded, setExpanded] = React.useState(true);
|
const [isExpanded, setExpanded] = React.useState(true);
|
||||||
const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies);
|
const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies);
|
||||||
|
|
||||||
const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length;
|
const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length;
|
||||||
const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0;
|
const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0;
|
||||||
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length;
|
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length;
|
||||||
|
@ -53,7 +53,7 @@ function CommentsReplies(props: Props) {
|
||||||
if (!fetchedReplies || alreadyResolved) return;
|
if (!fetchedReplies || alreadyResolved) return;
|
||||||
|
|
||||||
const urisToResolve = [];
|
const urisToResolve = [];
|
||||||
fetchedReplies.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
|
fetchedReplies.forEach(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
|
||||||
|
|
||||||
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
||||||
}, [alreadyResolved, doResolveUris, fetchedReplies]);
|
}, [alreadyResolved, doResolveUris, fetchedReplies]);
|
||||||
|
@ -65,7 +65,7 @@ function CommentsReplies(props: Props) {
|
||||||
}, [isResolvingReplies, fetchedReplies]);
|
}, [isResolvingReplies, fetchedReplies]);
|
||||||
|
|
||||||
return !numDirectReplies ? null : (
|
return !numDirectReplies ? null : (
|
||||||
<div className="comment__replies-container">
|
<div className="commentReplies__container">
|
||||||
{!isExpanded ? (
|
{!isExpanded ? (
|
||||||
<div className="comment__actions--nested">
|
<div className="comment__actions--nested">
|
||||||
<Button
|
<Button
|
||||||
|
@ -83,35 +83,22 @@ function CommentsReplies(props: Props) {
|
||||||
{!isResolvingReplies &&
|
{!isResolvingReplies &&
|
||||||
commentsToDisplay &&
|
commentsToDisplay &&
|
||||||
commentsToDisplay.length > 0 &&
|
commentsToDisplay.length > 0 &&
|
||||||
commentsToDisplay.map((comment) => (
|
commentsToDisplay.map((comment: Comment) => (
|
||||||
<Comment
|
<Comment
|
||||||
|
claimIsMine={claimIsMine}
|
||||||
|
comment={comment}
|
||||||
|
commentingEnabled={userCanComment}
|
||||||
|
key={comment.comment_id}
|
||||||
|
linkedCommentId={linkedCommentId}
|
||||||
|
supportDisabled={supportDisabled}
|
||||||
threadDepth={threadDepth}
|
threadDepth={threadDepth}
|
||||||
uri={uri}
|
uri={uri}
|
||||||
authorUri={comment.channel_url}
|
|
||||||
author={comment.channel_name}
|
|
||||||
claimId={comment.claim_id}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
key={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
timePosted={comment.timestamp * 1000}
|
|
||||||
claimIsMine={claimIsMine}
|
|
||||||
commentIsMine={
|
|
||||||
comment.channel_id &&
|
|
||||||
myChannels &&
|
|
||||||
myChannels.some(({ claim_id }) => claim_id === comment.channel_id)
|
|
||||||
}
|
|
||||||
linkedCommentId={linkedCommentId}
|
|
||||||
commentingEnabled={userCanComment}
|
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
numDirectReplies={comment.replies}
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
supportDisabled={supportDisabled}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isExpanded && fetchedReplies && hasMore && (
|
{isExpanded && fetchedReplies && hasMore && (
|
||||||
<div className="comment__actions--nested">
|
<div className="comment__actions--nested">
|
||||||
<Button
|
<Button
|
||||||
|
@ -122,11 +109,10 @@ function CommentsReplies(props: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && (
|
{(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && (
|
||||||
<div className="comment__replies-container">
|
<div className="comment__actions--nested">
|
||||||
<div className="comment__actions--nested">
|
<Spinner type="small" />
|
||||||
<Spinner type="small" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -92,8 +92,8 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
<span
|
<span
|
||||||
title={fullPrice}
|
title={fullPrice}
|
||||||
className={classnames(className, {
|
className={classnames(className, {
|
||||||
'super-chat': superChat,
|
superChat: superChat,
|
||||||
'super-chat--light': superChatLight,
|
'superChat--light': superChatLight,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className="credit-amount">{amountText}</span>
|
<span className="credit-amount">{amountText}</span>
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 50;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 50;
|
||||||
|
|
||||||
function scaleToDevicePixelRatio(value) {
|
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
|
||||||
if (devicePixelRatio < 1.0) {
|
|
||||||
return Math.ceil(value / devicePixelRatio);
|
|
||||||
}
|
|
||||||
return Math.ceil(value * devicePixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: any,
|
children: any,
|
||||||
skipWait?: boolean,
|
skipWait?: boolean,
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectStakedLevelForChannelUri, selectClaimForUri } from 'redux/selectors/claims';
|
import { makeSelectStakedLevelForChannelUri, selectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims';
|
||||||
import LivestreamComment from './view';
|
import LivestreamComment from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
claim: selectClaimForUri(state, props.uri),
|
claim: selectClaimForUri(state, props.uri),
|
||||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||||
|
myChannels: selectMyChannelClaims(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(LivestreamComment);
|
export default connect(select)(LivestreamComment);
|
||||||
|
|
|
@ -1,102 +1,87 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import 'scss/component/_livestream-comment.scss';
|
||||||
import React from 'react';
|
|
||||||
import { parseURI } from 'util/lbryURI';
|
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
|
||||||
import Tooltip from 'component/common/tooltip';
|
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
|
||||||
import { Menu, MenuButton } from '@reach/menu-button';
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import { parseSticker } from 'util/comments';
|
||||||
|
import { parseURI } from 'util/lbryURI';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import CommentMenuList from 'component/commentMenuList';
|
import CommentMenuList from 'component/commentMenuList';
|
||||||
import Button from 'component/button';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import { parseSticker } from 'util/comments';
|
import React from 'react';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
authorUri: string,
|
comment: Comment,
|
||||||
commentId: string,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
message: string,
|
|
||||||
commentIsMine: boolean,
|
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
supportAmount: number,
|
uri: string,
|
||||||
isModerator: boolean,
|
|
||||||
isGlobalMod: boolean,
|
|
||||||
isFiat: boolean,
|
|
||||||
isPinned: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function LivestreamComment(props: Props) {
|
function LivestreamComment(props: Props) {
|
||||||
const {
|
const { claim, comment, myChannels, stakedLevel, uri } = props;
|
||||||
claim,
|
|
||||||
uri,
|
|
||||||
authorUri,
|
|
||||||
message,
|
|
||||||
commentIsMine,
|
|
||||||
commentId,
|
|
||||||
stakedLevel,
|
|
||||||
supportAmount,
|
|
||||||
isModerator,
|
|
||||||
isGlobalMod,
|
|
||||||
isFiat,
|
|
||||||
isPinned,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const {
|
||||||
const { claimName } = parseURI(authorUri);
|
channel_url: authorUri,
|
||||||
|
channel_id: authorId,
|
||||||
|
comment_id: commentId,
|
||||||
|
comment: message,
|
||||||
|
is_fiat: isFiat,
|
||||||
|
is_global_mod: isGlobalMod,
|
||||||
|
is_moderator: isModerator,
|
||||||
|
is_pinned: isPinned,
|
||||||
|
support_amount: supportAmount,
|
||||||
|
} = comment;
|
||||||
|
const commentIsMine = authorId && myChannels && myChannels.some(({ claim_id }) => claim_id === authorId);
|
||||||
|
|
||||||
|
const commentByContentOwner = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
const stickerFromMessage = parseSticker(message);
|
const stickerFromMessage = parseSticker(message);
|
||||||
|
let claimName;
|
||||||
|
try {
|
||||||
|
authorUri && ({ claimName } = parseURI(authorUri));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const commentBadge = (label: string, icon: string, className?: string) => (
|
||||||
|
<Tooltip label={label}>
|
||||||
|
<span className={classnames('comment__badge', { className })}>
|
||||||
|
<Icon icon={icon} size={16} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classnames('livestream-comment', {
|
className={classnames('livestreamComment', {
|
||||||
'livestream-comment--superchat': supportAmount > 0,
|
'livestreamComment--superchat': supportAmount > 0,
|
||||||
'livestream-comment--sticker': Boolean(stickerFromMessage),
|
'livestreamComment--sticker': Boolean(stickerFromMessage),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{supportAmount > 0 && (
|
{supportAmount > 0 && (
|
||||||
<div className="super-chat livestream-superchat__banner">
|
<div className="superChat livestreamComment__superchatBanner">
|
||||||
<div className="livestream-superchat__banner-corner" />
|
<div className="livestreamComment__superchatBanner-corner" />
|
||||||
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestream-superchat__amount" />
|
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestreamSuperchat__amount" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="livestream-comment__body">
|
<div className="livestreamComment__body">
|
||||||
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />}
|
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||||
<div
|
<div
|
||||||
className={classnames('livestream-comment__info', {
|
className={classnames('livestreamComment__info', {
|
||||||
'livestream-comment__info--sticker': Boolean(stickerFromMessage),
|
'livestreamComment__info--sticker': Boolean(stickerFromMessage),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isGlobalMod && (
|
{isGlobalMod && commentBadge(__('Admin'), ICONS.BADGE_MOD, 'comment__badge--globalMod')}
|
||||||
<Tooltip label={__('Admin')}>
|
{isModerator && commentBadge(__('Moderator'), ICONS.BADGE_MOD, 'comment__badge--mod')}
|
||||||
<span className="comment__badge comment__badge--global-mod">
|
{commentByContentOwner && commentBadge(__('Streamer'), ICONS.BADGE_STREAMER)}
|
||||||
<Icon icon={ICONS.BADGE_MOD} size={16} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isModerator && (
|
|
||||||
<Tooltip label={__('Moderator')}>
|
|
||||||
<span className="comment__badge comment__badge--mod">
|
|
||||||
<Icon icon={ICONS.BADGE_MOD} size={16} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{commentByOwnerOfContent && (
|
|
||||||
<Tooltip label={__('Streamer')}>
|
|
||||||
<span className="comment__badge">
|
|
||||||
<Icon icon={ICONS.BADGE_STREAMER} size={16} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={classnames('button--uri-indicator comment__author', {
|
className={classnames('button--uri-indicator comment__author', {
|
||||||
'comment__author--creator': commentByOwnerOfContent,
|
'comment__author--creator': commentByContentOwner,
|
||||||
})}
|
})}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
navigate={authorUri}
|
navigate={authorUri}
|
||||||
|
@ -112,18 +97,18 @@ function LivestreamComment(props: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{stickerFromMessage ? (
|
{stickerFromMessage ? (
|
||||||
<div className="sticker__comment">
|
<div className="comment__message--sticker">
|
||||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="livestream-comment__text">
|
<div className="livestreamComment__text">
|
||||||
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="livestream-comment__menu">
|
<div className="livestreamComment__menu">
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton className="menu__button">
|
<MenuButton className="menu__button">
|
||||||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims';
|
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
||||||
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||||
import {
|
import {
|
||||||
|
@ -20,7 +20,6 @@ const select = (state, props) => ({
|
||||||
fetchingComments: selectIsFetchingComments(state),
|
fetchingComments: selectIsFetchingComments(state),
|
||||||
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
||||||
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
||||||
myChannels: selectMyChannelClaims(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import 'scss/component/_livestream-comments.scss';
|
||||||
import classnames from 'classnames';
|
|
||||||
import Spinner from 'component/spinner';
|
|
||||||
import CommentCreate from 'component/commentCreate';
|
|
||||||
import LivestreamComment from 'component/livestreamComment';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import UriIndicator from 'component/uriIndicator';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
|
||||||
import Tooltip from 'component/common/tooltip';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
|
||||||
import { parseSticker } from 'util/comments';
|
import { parseSticker } from 'util/comments';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import CommentCreate from 'component/commentCreate';
|
||||||
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
|
import LivestreamComment from 'component/livestreamComment';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import React from 'react';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
import UriIndicator from 'component/uriIndicator';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
|
||||||
claim: ?StreamClaim,
|
|
||||||
activeViewers: number,
|
activeViewers: number,
|
||||||
|
claim: ?StreamClaim,
|
||||||
|
comments: Array<Comment>,
|
||||||
embed?: boolean,
|
embed?: boolean,
|
||||||
|
fetchingComments: boolean,
|
||||||
|
pinnedComments: Array<Comment>,
|
||||||
|
superChats: Array<Comment>,
|
||||||
|
uri: string,
|
||||||
|
doCommentList: (string, string, number, number) => void,
|
||||||
doCommentSocketConnect: (string, string) => void,
|
doCommentSocketConnect: (string, string) => void,
|
||||||
doCommentSocketDisconnect: (string) => void,
|
doCommentSocketDisconnect: (string) => void,
|
||||||
doCommentList: (string, string, number, number) => void,
|
|
||||||
comments: Array<Comment>,
|
|
||||||
pinnedComments: Array<Comment>,
|
|
||||||
fetchingComments: boolean,
|
|
||||||
doSuperChatList: (string) => void,
|
doSuperChatList: (string) => void,
|
||||||
superChats: Array<Comment>,
|
|
||||||
myChannels: ?Array<ChannelClaim>,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIEW_MODE_CHAT = 'view_chat';
|
const VIEW_MODE_CHAT = 'view_chat';
|
||||||
|
@ -36,17 +36,16 @@ const COMMENT_SCROLL_TIMEOUT = 25;
|
||||||
export default function LivestreamComments(props: Props) {
|
export default function LivestreamComments(props: Props) {
|
||||||
const {
|
const {
|
||||||
claim,
|
claim,
|
||||||
uri,
|
comments: commentsByChronologicalOrder,
|
||||||
embed,
|
embed,
|
||||||
|
fetchingComments,
|
||||||
|
pinnedComments,
|
||||||
|
superChats: superChatsByTipAmount,
|
||||||
|
uri,
|
||||||
|
doCommentList,
|
||||||
doCommentSocketConnect,
|
doCommentSocketConnect,
|
||||||
doCommentSocketDisconnect,
|
doCommentSocketDisconnect,
|
||||||
comments: commentsByChronologicalOrder,
|
|
||||||
pinnedComments,
|
|
||||||
doCommentList,
|
|
||||||
fetchingComments,
|
|
||||||
doSuperChatList,
|
doSuperChatList,
|
||||||
myChannels,
|
|
||||||
superChats: superChatsByTipAmount,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats;
|
let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats;
|
||||||
|
@ -55,22 +54,28 @@ export default function LivestreamComments(props: Props) {
|
||||||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||||
const [scrollPos, setScrollPos] = React.useState(0);
|
const [scrollPos, setScrollPos] = React.useState(0);
|
||||||
const [showPinned, setShowPinned] = React.useState(true);
|
const [showPinned, setShowPinned] = React.useState(true);
|
||||||
|
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
|
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
|
||||||
|
|
||||||
// which kind of superchat to display, either
|
// which kind of superchat to display, either
|
||||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
||||||
const stickerSuperChats =
|
const stickerSuperChats =
|
||||||
superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment)));
|
superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment)));
|
||||||
|
|
||||||
const discussionElement = document.querySelector('.livestream__comments');
|
const discussionElement = document.querySelector('.livestream__comments');
|
||||||
|
|
||||||
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
|
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
|
||||||
|
|
||||||
|
let superChatsReversed;
|
||||||
|
// array of superchats organized by fiat or not first, then support amount
|
||||||
|
if (superChatsByTipAmount) {
|
||||||
|
const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount));
|
||||||
|
|
||||||
|
// for top to bottom display, oldest superchat on top most recent on bottom
|
||||||
|
superChatsReversed = clonedSuperchats.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
const restoreScrollPos = React.useCallback(() => {
|
const restoreScrollPos = React.useCallback(() => {
|
||||||
if (discussionElement) {
|
if (discussionElement) discussionElement.scrollTop = 0;
|
||||||
discussionElement.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}, [discussionElement]);
|
}, [discussionElement]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -139,75 +144,41 @@ export default function LivestreamComments(props: Props) {
|
||||||
hasSuperChats = (superChatsTotalAmount || 0) > 0;
|
hasSuperChats = (superChatsTotalAmount || 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let superChatsReversed;
|
|
||||||
// array of superchats organized by fiat or not first, then support amount
|
|
||||||
if (superChatsByTipAmount) {
|
|
||||||
const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount));
|
|
||||||
|
|
||||||
// for top to bottom display, oldest superchat on top most recent on bottom
|
|
||||||
superChatsReversed = clonedSuperchats.sort((a, b) => {
|
|
||||||
return b.timestamp - a.timestamp;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
|
||||||
function isMyComment(channelId: string) {
|
|
||||||
if (myChannels != null && channelId != null) {
|
|
||||||
for (let i = 0; i < myChannels.length; i++) {
|
|
||||||
if (myChannels[i].claim_id === channelId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!claim) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStickerUrl(comment: string) {
|
function getStickerUrl(comment: string) {
|
||||||
const stickerFromComment = parseSticker(comment);
|
const stickerFromComment = parseSticker(comment);
|
||||||
return stickerFromComment && stickerFromComment.url;
|
return stickerFromComment && stickerFromComment.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const getChatContentToggle = (toggleMode: string, label: any) => (
|
||||||
|
<Button
|
||||||
|
className={classnames('button-toggle', { 'button-toggle--active': viewMode === toggleMode })}
|
||||||
|
label={label}
|
||||||
|
onClick={() => {
|
||||||
|
setViewMode(toggleMode);
|
||||||
|
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
|
||||||
|
const divHeight = livestreamCommentsDiv.scrollHeight;
|
||||||
|
livestreamCommentsDiv.scrollTop = toggleMode === VIEW_MODE_CHAT ? divHeight : divHeight * -1;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return !claim ? null : (
|
||||||
<div className="card livestream__discussion">
|
<div className="card livestream__discussion">
|
||||||
<div className="card__header--between livestream-discussion__header">
|
<div className="card__header--between livestreamDiscussion__header">
|
||||||
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
<div className="card__title-section--small livestreamDiscussion__title">{__('Live discussion')}</div>
|
||||||
{hasSuperChats && (
|
{hasSuperChats && (
|
||||||
<div className="recommended-content__toggles">
|
<div className="recommended-content__toggles">
|
||||||
{/* the superchats in chronological order button */}
|
{/* the superchats in chronological order button */}
|
||||||
<Button
|
{getChatContentToggle(VIEW_MODE_CHAT, __('Chat'))}
|
||||||
className={classnames('button-toggle', {
|
|
||||||
'button-toggle--active': viewMode === VIEW_MODE_CHAT,
|
|
||||||
})}
|
|
||||||
label={__('Chat')}
|
|
||||||
onClick={() => {
|
|
||||||
setViewMode(VIEW_MODE_CHAT);
|
|
||||||
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
|
|
||||||
livestreamCommentsDiv.scrollTop = livestreamCommentsDiv.scrollHeight;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* the button to show superchats listed by most to least support amount */}
|
{/* the button to show superchats listed by most to least support amount */}
|
||||||
<Button
|
{getChatContentToggle(
|
||||||
className={classnames('button-toggle', {
|
VIEW_MODE_SUPER_CHAT,
|
||||||
'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT,
|
<>
|
||||||
})}
|
<CreditAmount amount={superChatsTotalAmount || 0} size={8} /> /
|
||||||
label={
|
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
||||||
<>
|
</>
|
||||||
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
|
)}
|
||||||
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setViewMode(VIEW_MODE_SUPER_CHAT);
|
|
||||||
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
|
|
||||||
const divHeight = livestreamCommentsDiv.scrollHeight;
|
|
||||||
livestreamCommentsDiv.scrollTop = divHeight * -1;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -217,10 +188,10 @@ export default function LivestreamComments(props: Props) {
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
<div ref={commentsRef} className="livestreamComments__wrapper">
|
||||||
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && (
|
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && (
|
||||||
<div className="livestream-superchats__wrapper">
|
<div className="livestreamSuperchats__wrapper">
|
||||||
<div className="livestream-superchats__inner">
|
<div className="livestreamSuperchats__inner">
|
||||||
{superChatsByTipAmount.map((superChat: Comment) => {
|
{superChatsByTipAmount.map((superChat: Comment) => {
|
||||||
const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
|
const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
|
||||||
|
|
||||||
|
@ -230,28 +201,28 @@ export default function LivestreamComments(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SuperChatWrapper key={superChat.comment_id}>
|
<SuperChatWrapper key={superChat.comment_id}>
|
||||||
<div className="livestream-superchat">
|
<div className="livestreamSuperchat">
|
||||||
<div className="livestream-superchat__thumbnail">
|
<div className="livestreamSuperchat__thumbnail">
|
||||||
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classnames('livestream-superchat__info', {
|
className={classnames('livestreamSuperchat__info', {
|
||||||
'livestream-superchat__info--sticker': isSticker,
|
'livestreamSuperchat__info--sticker': isSticker,
|
||||||
'livestream-superchat__info--not-sticker': stickerSuperChats && !isSticker,
|
'livestreamSuperchat__info--notSticker': stickerSuperChats && !isSticker,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="livestream-superchat__info--user">
|
<div className="livestreamSuperchat__info--user">
|
||||||
<UriIndicator uri={superChat.channel_url} link />
|
<UriIndicator uri={superChat.channel_url} link />
|
||||||
<CreditAmount
|
<CreditAmount
|
||||||
size={10}
|
size={10}
|
||||||
className="livestream-superchat__amount-large"
|
className="livestreamSuperchat__amount-large"
|
||||||
amount={superChat.support_amount}
|
amount={superChat.support_amount}
|
||||||
isFiat={superChat.is_fiat}
|
isFiat={superChat.is_fiat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
|
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
|
||||||
<div className="livestream-superchat__info--image">
|
<div className="livestreamSuperchat__info--image">
|
||||||
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad />
|
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -265,20 +236,8 @@ export default function LivestreamComments(props: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{pinnedComment && showPinned && viewMode === VIEW_MODE_CHAT && (
|
{pinnedComment && showPinned && viewMode === VIEW_MODE_CHAT && (
|
||||||
<div className="livestream-pinned__wrapper">
|
<div className="livestreamPinned__wrapper">
|
||||||
<LivestreamComment
|
<LivestreamComment key={pinnedComment.comment_id} uri={uri} comment={pinnedComment} />
|
||||||
key={pinnedComment.comment_id}
|
|
||||||
uri={uri}
|
|
||||||
authorUri={pinnedComment.channel_url}
|
|
||||||
commentId={pinnedComment.comment_id}
|
|
||||||
message={pinnedComment.comment}
|
|
||||||
supportAmount={pinnedComment.support_amount}
|
|
||||||
isModerator={pinnedComment.is_moderator}
|
|
||||||
isGlobalMod={pinnedComment.is_global_mod}
|
|
||||||
isFiat={pinnedComment.is_fiat}
|
|
||||||
isPinned={pinnedComment.is_pinned}
|
|
||||||
commentIsMine={pinnedComment.channel_id && isMyComment(pinnedComment.channel_id)}
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
title={__('Dismiss pinned comment')}
|
title={__('Dismiss pinned comment')}
|
||||||
button="inverse"
|
button="inverse"
|
||||||
|
@ -294,36 +253,14 @@ export default function LivestreamComments(props: Props) {
|
||||||
<div className="livestream__comments">
|
<div className="livestream__comments">
|
||||||
{viewMode === VIEW_MODE_CHAT &&
|
{viewMode === VIEW_MODE_CHAT &&
|
||||||
commentsToDisplay.map((comment) => (
|
commentsToDisplay.map((comment) => (
|
||||||
<LivestreamComment
|
<LivestreamComment key={comment.comment_id} uri={uri} comment={comment} />
|
||||||
key={comment.comment_id}
|
|
||||||
uri={uri}
|
|
||||||
authorUri={comment.channel_url}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
isFiat={comment.is_fiat}
|
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* listing comments on top of eachother */}
|
{/* listing comments on top of eachother */}
|
||||||
{viewMode === VIEW_MODE_SUPER_CHAT &&
|
{viewMode === VIEW_MODE_SUPER_CHAT &&
|
||||||
superChatsReversed &&
|
superChatsReversed &&
|
||||||
superChatsReversed.map((comment) => (
|
superChatsReversed.map((comment) => (
|
||||||
<LivestreamComment
|
<LivestreamComment key={comment.comment_id} uri={uri} comment={comment} />
|
||||||
key={comment.comment_id}
|
|
||||||
uri={uri}
|
|
||||||
authorUri={comment.channel_url}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
isFiat={comment.is_fiat}
|
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -333,14 +270,14 @@ export default function LivestreamComments(props: Props) {
|
||||||
{scrollPos < 0 && viewMode === VIEW_MODE_CHAT && (
|
{scrollPos < 0 && viewMode === VIEW_MODE_CHAT && (
|
||||||
<Button
|
<Button
|
||||||
button="secondary"
|
button="secondary"
|
||||||
className="livestream__comments__scroll-to-recent"
|
className="livestreamComments__scrollToRecent"
|
||||||
label={__('Recent Comments')}
|
label={__('Recent Comments')}
|
||||||
onClick={restoreScrollPos}
|
onClick={restoreScrollPos}
|
||||||
iconRight={ICONS.DOWN}
|
iconRight={ICONS.DOWN}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="livestream__comment-create">
|
<div className="livestream__commentCreate">
|
||||||
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
|
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
|
||||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||||
|
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||||
function scaleToDevicePixelRatio(value: number, window: any) {
|
import React from 'react';
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
|
||||||
return Math.ceil(value * devicePixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
src: string,
|
src: string,
|
||||||
|
@ -40,8 +36,8 @@ function OptimizedImage(props: Props) {
|
||||||
let width = elem.parentElement.clientWidth;
|
let width = elem.parentElement.clientWidth;
|
||||||
let height = elem.parentElement.clientHeight;
|
let height = elem.parentElement.clientHeight;
|
||||||
|
|
||||||
width = scaleToDevicePixelRatio(width, window);
|
width = scaleToDevicePixelRatio(width);
|
||||||
height = scaleToDevicePixelRatio(height, window);
|
height = scaleToDevicePixelRatio(height);
|
||||||
|
|
||||||
// Round to next 100px for better caching
|
// Round to next 100px for better caching
|
||||||
width = Math.ceil(width / 100) * 100;
|
width = Math.ceil(width / 100) * 100;
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export const COMMENT_HIGHLIGHTED = 'comment--highlighted';
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
|
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||||
import { doResolveUri } from 'redux/actions/claims';
|
import { doClaimSearch } from 'redux/actions/claims';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { doUserSetReferrer } from 'redux/actions/user';
|
import { doUserSetReferrer } from 'redux/actions/user';
|
||||||
|
import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards';
|
|
||||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
|
||||||
import LivestreamPage from './view';
|
import LivestreamPage from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
|
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
channelClaim: makeSelectClaimForUri(props.uri)(state),
|
channelClaim: makeSelectClaimForUri(props.uri)(state),
|
||||||
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
const perform = (dispatch) => ({
|
||||||
doSetPlayingUri,
|
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||||
doResolveUri,
|
doClaimSearch: (options, cb) => dispatch(doClaimSearch(options, cb)),
|
||||||
doUserSetReferrer,
|
setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)),
|
||||||
})(LivestreamPage);
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(LivestreamPage);
|
||||||
|
|
|
@ -1,89 +1,81 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
|
import { LIVESTREAM_LIVE_API } from 'constants/livestream';
|
||||||
import React from 'react';
|
|
||||||
import Page from 'component/page';
|
|
||||||
import LivestreamLayout from 'component/livestreamLayout';
|
|
||||||
import LivestreamComments from 'component/livestreamComments';
|
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import Lbry from 'lbry';
|
import LivestreamComments from 'component/livestreamComments';
|
||||||
|
import LivestreamLayout from 'component/livestreamLayout';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const STREAMING_POLL_INTERVAL_IN_MS = 10000;
|
||||||
|
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
|
||||||
claim: StreamClaim,
|
|
||||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
|
||||||
isAuthenticated: boolean,
|
|
||||||
doUserSetReferrer: (string) => void,
|
|
||||||
channelClaim: ChannelClaim,
|
channelClaim: ChannelClaim,
|
||||||
chatDisabled: boolean,
|
chatDisabled: boolean,
|
||||||
|
claim: StreamClaim,
|
||||||
|
isAuthenticated: boolean,
|
||||||
|
uri: string,
|
||||||
|
clearPlayingUri: () => void,
|
||||||
|
doClaimSearch: (any, (ClaimSearchResponse) => void) => void,
|
||||||
|
setReferrer: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LivestreamPage(props: Props) {
|
export default function LivestreamPage(props: Props) {
|
||||||
const { uri, claim, doSetPlayingUri, isAuthenticated, doUserSetReferrer, channelClaim, chatDisabled } = props;
|
const {
|
||||||
|
channelClaim,
|
||||||
|
chatDisabled,
|
||||||
|
claim,
|
||||||
|
isAuthenticated,
|
||||||
|
uri,
|
||||||
|
clearPlayingUri,
|
||||||
|
doClaimSearch,
|
||||||
|
setReferrer,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [isLive, setIsLive] = React.useState(false);
|
const [isLive, setIsLive] = React.useState(false);
|
||||||
const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
|
|
||||||
const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false);
|
const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false);
|
||||||
|
|
||||||
const STREAMING_POLL_INTERVAL_IN_MS = 10000;
|
const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
|
||||||
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000;
|
const stringifiedClaim = JSON.stringify(claim);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let checkClaimsInterval;
|
let checkClaimsInterval;
|
||||||
|
|
||||||
function checkHasLivestreamClaim() {
|
function checkHasLivestreamClaim() {
|
||||||
Lbry.claim_search({
|
doClaimSearch({ channel_ids: [livestreamChannelId], has_no_source: true, claim_type: ['stream'] }, (data) =>
|
||||||
channel_ids: [livestreamChannelId],
|
data && data.items && data.items.length > 0 ? setHasLivestreamClaim(true) : undefined
|
||||||
has_no_source: true,
|
);
|
||||||
claim_type: ['stream'],
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res && res.items && res.items.length > 0) {
|
|
||||||
setHasLivestreamClaim(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (livestreamChannelId && !isLive) {
|
if (livestreamChannelId && !isLive) {
|
||||||
if (!checkClaimsInterval) checkHasLivestreamClaim();
|
if (!checkClaimsInterval) checkHasLivestreamClaim();
|
||||||
checkClaimsInterval = setInterval(checkHasLivestreamClaim, LIVESTREAM_CLAIM_POLL_IN_MS);
|
checkClaimsInterval = setInterval(checkHasLivestreamClaim, LIVESTREAM_CLAIM_POLL_IN_MS);
|
||||||
|
|
||||||
return () => {
|
return () => (checkClaimsInterval ? clearInterval(checkClaimsInterval) : undefined);
|
||||||
if (checkClaimsInterval) {
|
|
||||||
clearInterval(checkClaimsInterval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [livestreamChannelId, isLive]);
|
}, [livestreamChannelId, isLive, doClaimSearch]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let interval;
|
let interval;
|
||||||
|
|
||||||
function checkIsLive() {
|
function checkIsLive() {
|
||||||
// TODO: duplicate code below
|
// TODO: duplicate code below
|
||||||
// $FlowFixMe livestream API can handle garbage
|
// $FlowFixMe livestream API can handle garbage
|
||||||
fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`)
|
fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) =>
|
||||||
if (!res || !res.data) {
|
!res || !res.data ? setIsLive(false) : res.data.hasOwnProperty('live') && setIsLive(res.data.live)
|
||||||
setIsLive(false);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.data.hasOwnProperty('live')) {
|
|
||||||
setIsLive(res.data.live);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (livestreamChannelId && hasLivestreamClaim) {
|
if (livestreamChannelId && hasLivestreamClaim) {
|
||||||
if (!interval) checkIsLive();
|
if (!interval) checkIsLive();
|
||||||
interval = setInterval(checkIsLive, STREAMING_POLL_INTERVAL_IN_MS);
|
interval = setInterval(checkIsLive, STREAMING_POLL_INTERVAL_IN_MS);
|
||||||
|
|
||||||
return () => {
|
return () => (interval ? clearInterval(interval) : undefined);
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [livestreamChannelId, hasLivestreamClaim]);
|
}, [livestreamChannelId, hasLivestreamClaim]);
|
||||||
|
|
||||||
const stringifiedClaim = JSON.stringify(claim);
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (uri && stringifiedClaim) {
|
if (uri && stringifiedClaim) {
|
||||||
const jsonClaim = JSON.parse(stringifiedClaim);
|
const jsonClaim = JSON.parse(stringifiedClaim);
|
||||||
|
@ -97,18 +89,16 @@ export default function LivestreamPage(props: Props) {
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
const uri = jsonClaim.signing_channel && jsonClaim.signing_channel.permanent_url;
|
const uri = jsonClaim.signing_channel && jsonClaim.signing_channel.permanent_url;
|
||||||
if (uri) {
|
if (uri) setReferrer(uri.replace('lbry://', ''));
|
||||||
doUserSetReferrer(uri.replace('lbry://', ''));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [uri, stringifiedClaim, isAuthenticated]);
|
}, [uri, stringifiedClaim, isAuthenticated, setReferrer]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Set playing uri to null so the popout player doesnt start playing the dummy claim if a user navigates back
|
// 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 LIVESTREAM iframe
|
// This can be removed when we start using the app video player, not a LIVESTREAM iframe
|
||||||
doSetPlayingUri({ uri: null });
|
clearPlayingUri();
|
||||||
}, [doSetPlayingUri]);
|
}, [clearPlayingUri]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
|
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
|
||||||
import {
|
import {
|
||||||
selectIsFetchingComments,
|
selectIsFetchingComments,
|
||||||
selectCommentsForUri,
|
selectCommentsForUri,
|
||||||
makeSelectTotalCommentsCountForUri,
|
makeSelectTotalCommentsCountForUri,
|
||||||
makeSelectTopLevelTotalPagesForUri,
|
makeSelectTopLevelTotalPagesForUri,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
|
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
|
||||||
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectClaimsById } from 'redux/selectors/claims';
|
import { selectClaimsById } from 'redux/selectors/claims';
|
||||||
|
|
||||||
import OwnComments from './view';
|
import OwnComments from './view';
|
||||||
|
|
||||||
const select = (state) => {
|
const select = (state) => {
|
||||||
|
@ -18,16 +17,16 @@ const select = (state) => {
|
||||||
return {
|
return {
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
allComments: selectCommentsForUri(state, uri),
|
allComments: selectCommentsForUri(state, uri),
|
||||||
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
|
|
||||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
|
||||||
isFetchingComments: selectIsFetchingComments(state),
|
|
||||||
claimsById: selectClaimsById(state),
|
claimsById: selectClaimsById(state),
|
||||||
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
|
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
||||||
|
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
doCommentReset: (a) => dispatch(doCommentReset(a)),
|
doCommentListOwn: (channelId, page, pageSize) => dispatch(doCommentListOwn(channelId, page, pageSize)),
|
||||||
doCommentListOwn: (a, b, c) => dispatch(doCommentListOwn(a, b, c)),
|
doCommentReset: (claimId) => dispatch(doCommentReset(claimId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(OwnComments);
|
export default connect(select, perform)(OwnComments);
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import 'scss/component/_comments-list.scss';
|
||||||
|
import 'scss/component/_comments-own.scss';
|
||||||
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
|
||||||
|
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
import ChannelSelector from 'component/channelSelector';
|
import ChannelSelector from 'component/channelSelector';
|
||||||
import ClaimPreview from 'component/claimPreview';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
import Comment from 'component/comment';
|
import Comment from 'component/comment';
|
||||||
import Card from 'component/common/card';
|
import debounce from 'util/debounce';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
|
import React from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import useFetched from 'effects/use-fetched';
|
import useFetched from 'effects/use-fetched';
|
||||||
import debounce from 'util/debounce';
|
|
||||||
|
|
||||||
function scaleToDevicePixelRatio(value) {
|
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
|
||||||
if (devicePixelRatio < 1.0) {
|
|
||||||
return Math.ceil(value / devicePixelRatio);
|
|
||||||
}
|
|
||||||
return Math.ceil(value * devicePixelRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
|
@ -42,6 +37,7 @@ export default function OwnComments(props: Props) {
|
||||||
doCommentReset,
|
doCommentReset,
|
||||||
doCommentListOwn,
|
doCommentListOwn,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const spinnerRef = React.useRef();
|
const spinnerRef = React.useRef();
|
||||||
const [page, setPage] = React.useState(0);
|
const [page, setPage] = React.useState(0);
|
||||||
const [activeChannelId, setActiveChannelId] = React.useState('');
|
const [activeChannelId, setActiveChannelId] = React.useState('');
|
||||||
|
@ -53,51 +49,6 @@ export default function OwnComments(props: Props) {
|
||||||
const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL);
|
const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL);
|
||||||
const moreBelow = page < totalPages;
|
const moreBelow = page < totalPages;
|
||||||
|
|
||||||
function getCommentsElem(comments) {
|
|
||||||
return comments.map((comment) => {
|
|
||||||
const contentClaim = claimsById[comment.claim_id];
|
|
||||||
const isChannel = contentClaim && contentClaim.value_type === 'channel';
|
|
||||||
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={comment.comment_id} className="comments-own card__main-actions">
|
|
||||||
<div className="section__actions">
|
|
||||||
<div className="comments-own--claim">
|
|
||||||
{contentClaim && (
|
|
||||||
<ClaimPreview
|
|
||||||
uri={contentClaim.canonical_url}
|
|
||||||
searchParams={{
|
|
||||||
...(isChannel ? { view: 'discussion' } : {}),
|
|
||||||
...(isLivestream ? {} : { lc: comment.comment_id }),
|
|
||||||
}}
|
|
||||||
hideActions
|
|
||||||
hideMenu
|
|
||||||
properties={() => null}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
|
|
||||||
</div>
|
|
||||||
<Comment
|
|
||||||
isTopLevel
|
|
||||||
hideActions
|
|
||||||
authorUri={comment.channel_url}
|
|
||||||
author={comment.channel_name}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
timePosted={comment.timestamp * 1000}
|
|
||||||
commentIsMine
|
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
numDirectReplies={0} // Don't show replies here
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
isFiat={comment.is_fiat}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active channel changed
|
// Active channel changed
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
|
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
|
||||||
|
@ -166,34 +117,49 @@ export default function OwnComments(props: Props) {
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
if (!activeChannelClaim) {
|
const getCommentsElem = (comments: Array<Comment>) =>
|
||||||
return null;
|
comments.map((comment: Comment) => {
|
||||||
}
|
const contentClaim = claimsById[comment.claim_id];
|
||||||
|
const isChannel = contentClaim && contentClaim.value_type === 'channel';
|
||||||
|
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div key={comment.comment_id} className="comments--own card__main-actions">
|
||||||
|
<div className="section__actions">
|
||||||
|
<div className="comments--own__claim">
|
||||||
|
{contentClaim && (
|
||||||
|
<ClaimPreview
|
||||||
|
uri={contentClaim.canonical_url}
|
||||||
|
searchParams={{
|
||||||
|
...(isChannel ? { view: 'discussion' } : {}),
|
||||||
|
...(isLivestream ? {} : { lc: comment.comment_id }),
|
||||||
|
}}
|
||||||
|
hideActions
|
||||||
|
hideMenu
|
||||||
|
properties={() => null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
|
||||||
|
</div>
|
||||||
|
{/* Don't show replies here */}
|
||||||
|
<Comment numDirectReplies={0} comment={comment} isTopLevel hideActions commentIsMine />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return !activeChannelClaim ? null : (
|
||||||
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}>
|
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}>
|
||||||
<ChannelSelector hideAnon />
|
<ChannelSelector hideAnon />
|
||||||
<Card
|
<Card
|
||||||
isBodyList
|
isBodyList
|
||||||
title={
|
title={
|
||||||
totalComments > 0
|
(isFetchingComments && '') ||
|
||||||
? totalComments === 1
|
(totalComments > 0 && __('No comments')) ||
|
||||||
? __('1 comment')
|
(totalComments === 1 && __('1 comment')) ||
|
||||||
: __('%total_comments% comments', { total_comments: totalComments })
|
(totalComments > 1 && __('%total_comments% comments', { total_comments: totalComments }))
|
||||||
: isFetchingComments
|
|
||||||
? ''
|
|
||||||
: __('No comments')
|
|
||||||
}
|
|
||||||
titleActions={
|
|
||||||
<Button
|
|
||||||
button="alt"
|
|
||||||
icon={ICONS.REFRESH}
|
|
||||||
title={__('Refresh')}
|
|
||||||
onClick={() => {
|
|
||||||
setPage(0);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
titleActions={<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />}
|
||||||
body={
|
body={
|
||||||
<>
|
<>
|
||||||
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}
|
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}
|
||||||
|
|
|
@ -24,6 +24,21 @@ import { doFetchItemsInCollections } from 'redux/actions/collections';
|
||||||
let onChannelConfirmCallback;
|
let onChannelConfirmCallback;
|
||||||
let checkPendingInterval;
|
let checkPendingInterval;
|
||||||
|
|
||||||
|
type ClaimSearchParams = {
|
||||||
|
page_size?: number,
|
||||||
|
page: number,
|
||||||
|
no_totals?: boolean,
|
||||||
|
any_tags?: Array<string>,
|
||||||
|
claim_ids?: Array<string>,
|
||||||
|
channel_ids?: Array<string>,
|
||||||
|
not_channel_ids?: Array<string>,
|
||||||
|
not_tags?: Array<string>,
|
||||||
|
order_by?: Array<string>,
|
||||||
|
release_time?: string,
|
||||||
|
has_source?: boolean,
|
||||||
|
has_no_souce?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
export function doResolveUris(
|
export function doResolveUris(
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
returnCachedClaims: boolean = false,
|
returnCachedClaims: boolean = false,
|
||||||
|
@ -604,24 +619,8 @@ export function doFetchCollectionListMine(page: number = 1, pageSize: number = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doClaimSearch(
|
export function doClaimSearch(
|
||||||
options: {
|
options: ClaimSearchParams = { page_size: 10, page: 1, no_totals: true },
|
||||||
page_size?: number,
|
successCb?: (ClaimSearchResponse) => void
|
||||||
page: number,
|
|
||||||
no_totals?: boolean,
|
|
||||||
any_tags?: Array<string>,
|
|
||||||
claim_ids?: Array<string>,
|
|
||||||
channel_ids?: Array<string>,
|
|
||||||
not_channel_ids?: Array<string>,
|
|
||||||
not_tags?: Array<string>,
|
|
||||||
order_by?: Array<string>,
|
|
||||||
release_time?: string,
|
|
||||||
has_source?: boolean,
|
|
||||||
has_no_souce?: boolean,
|
|
||||||
} = {
|
|
||||||
no_totals: true,
|
|
||||||
page_size: 10,
|
|
||||||
page: 1,
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
const query = createNormalizedClaimSearchKey(options);
|
const query = createNormalizedClaimSearchKey(options);
|
||||||
return async (dispatch: Dispatch) => {
|
return async (dispatch: Dispatch) => {
|
||||||
|
@ -648,6 +647,9 @@ export function doClaimSearch(
|
||||||
pageSize: options.page_size,
|
pageSize: options.page_size,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (successCb) successCb(data);
|
||||||
|
|
||||||
return resolveInfo;
|
return resolveInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -758,67 +758,53 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
|
||||||
|
|
||||||
export function doCommentUpdate(comment_id: string, comment: string) {
|
export function doCommentUpdate(comment_id: string, comment: string) {
|
||||||
// if they provided an empty string, they must have wanted to abandon
|
// if they provided an empty string, they must have wanted to abandon
|
||||||
if (comment === '') {
|
if (comment === '') return doCommentAbandon(comment_id);
|
||||||
return doCommentAbandon(comment_id);
|
|
||||||
} else {
|
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
if (!activeChannelClaim) {
|
const state = getState();
|
||||||
return dispatch(doToast({ isError: true, message: __('No active channel selected.') }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedComment = await channelSignData(activeChannelClaim.claim_id, comment);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
if (!signedComment) {
|
if (!activeChannelClaim) {
|
||||||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
return dispatch(doToast({ isError: true, message: __('No active channel selected.') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
const signedComment = await channelSignData(activeChannelClaim.claim_id, comment);
|
||||||
type: ACTIONS.COMMENT_UPDATE_STARTED,
|
if (!signedComment) {
|
||||||
});
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
}
|
||||||
|
|
||||||
return Comments.comment_edit({
|
dispatch({ type: ACTIONS.COMMENT_UPDATE_STARTED });
|
||||||
comment_id: comment_id,
|
|
||||||
comment: comment,
|
return Comments.comment_edit({
|
||||||
signature: signedComment.signature,
|
comment_id: comment_id,
|
||||||
signing_ts: signedComment.signing_ts,
|
comment: comment,
|
||||||
})
|
signature: signedComment.signature,
|
||||||
.then((result: CommentEditResponse) => {
|
signing_ts: signedComment.signing_ts,
|
||||||
if (result != null) {
|
})
|
||||||
dispatch({
|
.then((result: CommentEditResponse) => {
|
||||||
type: ACTIONS.COMMENT_UPDATE_COMPLETED,
|
if (result != null) {
|
||||||
data: {
|
dispatch({ type: ACTIONS.COMMENT_UPDATE_COMPLETED, data: { comment: result } });
|
||||||
comment: result,
|
} else {
|
||||||
},
|
// the result will return null
|
||||||
});
|
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED });
|
||||||
} else {
|
|
||||||
// the result will return null
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.COMMENT_UPDATE_FAILED,
|
|
||||||
});
|
|
||||||
dispatch(
|
|
||||||
doToast({
|
|
||||||
message: 'Your channel is still being setup, try again in a few moments.',
|
|
||||||
isError: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.COMMENT_UPDATE_FAILED,
|
|
||||||
data: error,
|
|
||||||
});
|
|
||||||
dispatch(
|
dispatch(
|
||||||
doToast({
|
doToast({
|
||||||
message: 'Unable to edit this comment, please try again later.',
|
message: 'Your channel is still being setup, try again in a few moments.',
|
||||||
isError: true,
|
isError: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
};
|
})
|
||||||
}
|
.catch((error) => {
|
||||||
|
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED, data: error });
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
message: 'Unable to edit this comment, please try again later.',
|
||||||
|
isError: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function channelSignName(channelClaimId: string, channelName: string) {
|
async function channelSignName(channelClaimId: string, channelName: string) {
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
@import 'component/channel-mention';
|
@import 'component/channel-mention';
|
||||||
@import 'component/claim-list';
|
@import 'component/claim-list';
|
||||||
@import 'component/collection';
|
@import 'component/collection';
|
||||||
@import 'component/comments';
|
|
||||||
@import 'component/content';
|
@import 'component/content';
|
||||||
@import 'component/dat-gui';
|
@import 'component/dat-gui';
|
||||||
@import 'component/embed-player';
|
@import 'component/embed-player';
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import '../../scss/component/form-field';
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
25
ui/scss/component/_comments-list.scss
Normal file
25
ui/scss/component/_comments-list.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
@import '../../../web/scss/themes/odysee/init/vars';
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: var(--font-small);
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments--contracted {
|
||||||
|
max-height: 5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-mask-image: -webkit-gradient(linear, left 30%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__sort {
|
||||||
|
margin: var(--spacing-s) 0;
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
margin-top: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
59
ui/scss/component/_comments-own.scss
Normal file
59
ui/scss/component/_comments-own.scss
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
@import '../../../web/scss/themes/odysee/init/vars';
|
||||||
|
@import '../../scss/init/mixins';
|
||||||
|
|
||||||
|
.comments--own {
|
||||||
|
.section__actions {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments--own__claim {
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
min-width: 40%;
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media__thumb {
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
$width: 5rem;
|
||||||
|
@include handleClaimListGifThumbnail($width);
|
||||||
|
width: $width;
|
||||||
|
height: calc(#{$width} * (9 / 16));
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
@include handleChannelGif(calc(5rem * 9 / 16));
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
@include handleChannelGif(calc(5rem * 9 / 16));
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-preview__wrapper {
|
||||||
|
margin: 0 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin: 0 var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
border-left: 4px solid var(--color-border);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,37 +1,17 @@
|
||||||
|
@import '../../../web/scss/themes/odysee/init/vars';
|
||||||
|
@import '../../scss/component/button';
|
||||||
|
@import '../../scss/init/gui';
|
||||||
|
@import '../../scss/init/mixins';
|
||||||
|
|
||||||
$thumbnailWidth: 1.5rem;
|
$thumbnailWidth: 1.5rem;
|
||||||
$thumbnailWidthSmall: 1rem;
|
$thumbnailWidthSmall: 1rem;
|
||||||
|
|
||||||
.comments {
|
|
||||||
list-style-type: none;
|
|
||||||
font-size: var(--font-small);
|
|
||||||
margin-top: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments--contracted {
|
|
||||||
@extend .comments;
|
|
||||||
max-height: 5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-mask-image: -webkit-gradient(linear, left 30%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments--replies {
|
.comments--replies {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin-left: var(--spacing-s);
|
margin-left: var(--spacing-s);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__sort {
|
|
||||||
margin: var(--spacing-s) 0;
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
margin-top: 0;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -44,7 +24,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__author-thumbnail {
|
.commentAuthor__thumbnail {
|
||||||
@include handleChannelGif($thumbnailWidthSmall);
|
@include handleChannelGif($thumbnailWidthSmall);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
||||||
|
@ -67,7 +47,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__thumbnail-wrapper {
|
.commentThumbnail__wrapper {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
@ -77,7 +57,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__replies-container {
|
.commentReplies__container {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,17 +79,17 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--top-level {
|
.comment--topLevel {
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--slimed {
|
.comment__content--slimed {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__edit-input {
|
.commentBody__editInput {
|
||||||
margin-top: var(--spacing-xxs);
|
margin-top: var(--spacing-xxs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,13 +112,13 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment--highlighted {
|
.comment__content--highlighted {
|
||||||
background: var(--color-comment-highlighted);
|
background: var(--color-comment-highlighted);
|
||||||
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__body-container {
|
.commentBody__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
@ -164,7 +144,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__meta-information {
|
.commentMeta__information {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -188,14 +168,14 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__badge--global-mod {
|
.commentBadge__globalMod {
|
||||||
.st0 {
|
.st0 {
|
||||||
// @see: ICONS.BADGE_MOD
|
// @see: ICONS.BADGE_MOD
|
||||||
fill: #fe7500;
|
fill: #fe7500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__badge--mod {
|
.commentBadge__mod {
|
||||||
.st0 {
|
.st0 {
|
||||||
// @see: ICONS.BADGE_MOD
|
// @see: ICONS.BADGE_MOD
|
||||||
fill: #ff3850;
|
fill: #ff3850;
|
||||||
|
@ -422,64 +402,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments-own {
|
.comment__message--sticker {
|
||||||
.section__actions {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comments-own--claim {
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
|
||||||
min-width: 40%;
|
|
||||||
max-width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media__thumb {
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
$width: 5rem;
|
|
||||||
@include handleClaimListGifThumbnail($width);
|
|
||||||
width: $width;
|
|
||||||
height: calc(#{$width} * (9 / 16));
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-thumbnail {
|
|
||||||
@include handleChannelGif(calc(5rem * 9 / 16));
|
|
||||||
margin-right: var(--spacing-xs);
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
@include handleChannelGif(calc(5rem * 9 / 16));
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-preview__wrapper {
|
|
||||||
margin: 0 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
|
||||||
margin: 0 var(--spacing-xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment {
|
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: var(--spacing-m);
|
|
||||||
border-left: 4px solid var(--color-border);
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticker__comment {
|
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
130
ui/scss/component/_livestream-comment.scss
Normal file
130
ui/scss/component/_livestream-comment.scss
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
@import '../../../web/scss/themes/odysee/init/vars';
|
||||||
|
@import '../../scss/init/mixins';
|
||||||
|
|
||||||
|
.livestreamComment {
|
||||||
|
list-style-type: none;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:hover) {
|
||||||
|
.menu__button:not(:focus):not([aria-expanded='true']) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__info {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment--superchat {
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
|
||||||
|
+ .livestreamComment--superchat {
|
||||||
|
margin-bottom: var(--spacing-xxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__body {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: unset;
|
||||||
|
flex: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__info {
|
||||||
|
margin-top: calc(var(--spacing-xxs) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: 4rem;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
width: 5px;
|
||||||
|
background-color: var(--color-superchat);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__body {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.livestreamComment__info--sticker {
|
||||||
|
display: flex;
|
||||||
|
margin: var(--spacing-xxs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
@include handleChannelGif(2rem);
|
||||||
|
margin-top: var(--spacing-xxs);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__menu {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--spacing-xs);
|
||||||
|
top: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__superchatBanner {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
padding: 0.25rem var(--spacing-s);
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is just a two small circles that overlap to make it look like
|
||||||
|
// the banner and the left border are connected
|
||||||
|
.livestreamComment__superchatBanner-corner {
|
||||||
|
height: calc(var(--border-radius) * 2);
|
||||||
|
width: calc(var(--border-radius) * 2);
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-superchat);
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(0) translateY(50%);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
height: calc(var(--border-radius) * 2);
|
||||||
|
width: calc(var(--border-radius) * 2);
|
||||||
|
border-top-left-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(25%) translateY(50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComment__text {
|
||||||
|
padding-right: var(--spacing-xl);
|
||||||
|
padding-bottom: var(--spacing-xxs);
|
||||||
|
|
||||||
|
.markdown-preview {
|
||||||
|
p {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
180
ui/scss/component/_livestream-comments.scss
Normal file
180
ui/scss/component/_livestream-comments.scss
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
@import '../../../web/scss/themes/odysee/init/vars';
|
||||||
|
|
||||||
|
$discussion-header__height: 3rem;
|
||||||
|
$recent-msg-button__height: 2rem;
|
||||||
|
|
||||||
|
.livestream__discussion {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin: 0;
|
||||||
|
width: var(--livestreamComments-width);
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: var(--header-height);
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
.card__main-actions {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamDiscussion__header {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: var(--spacing-s);
|
||||||
|
margin-bottom: 0;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
height: $discussion-header__height;
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamDiscussion__title {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComments__wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - var(--header-height) - #{$discussion-header__height});
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__comments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
font-size: var(--font-small);
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: visible;
|
||||||
|
padding-top: var(--spacing-s);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamComments__scrollToRecent {
|
||||||
|
margin-top: -$recent-msg-button__height;
|
||||||
|
align-self: center;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
padding: var(--spacing-xxs) var(--spacing-s);
|
||||||
|
opacity: 0.9;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__commentCreate {
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchats__wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: scroll;
|
||||||
|
padding: var(--spacing-s) var(--spacing-xs);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
background-color: var(--color-card-background);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
width: var(--livestreamComments-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamPinned__wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
padding: var(--spacing-s) var(--spacing-xs);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
font-size: var(--font-small);
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.livestreamComment {
|
||||||
|
width: 100%;
|
||||||
|
padding-top: var(--spacing-xs);
|
||||||
|
max-height: 6rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
padding: 0 calc(var(--spacing-m) - var(--spacing-xs)) 0 var(--spacing-m);
|
||||||
|
color: var(--color-text-subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
padding: var(--spacing-xs);
|
||||||
|
width: var(--livestreamComments-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchat__amount-large {
|
||||||
|
.credit-amount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchats__inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchat__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchat__info--sticker {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 8rem;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
.livestreamSuperchat__info--image {
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-top: calc(var(--spacing-xxs) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchat__info--notSticker {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchat__tooltip-amount {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-left: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestream__superchat-comment {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
max-width: 5rem;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.livestreamSuperchat__amount-large {
|
||||||
|
min-width: 2.5rem;
|
||||||
|
}
|
|
@ -26,162 +26,6 @@ $recent-msg-button__height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__discussion {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) {
|
|
||||||
margin: 0;
|
|
||||||
width: var(--livestream-comments-width);
|
|
||||||
height: calc(100vh - var(--header-height));
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: var(--header-height);
|
|
||||||
bottom: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
border-top: none;
|
|
||||||
border-bottom: none;
|
|
||||||
border-right: none;
|
|
||||||
|
|
||||||
.card__main-actions {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-discussion__header {
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
padding-bottom: var(--spacing-s);
|
|
||||||
margin-bottom: 0;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
height: $discussion-header__height;
|
|
||||||
padding: 0 var(--spacing-s);
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-discussion__title {
|
|
||||||
@extend .card__title-section;
|
|
||||||
@extend .card__title-section--small;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__comments-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: calc(100vh - var(--header-height) - #{$discussion-header__height});
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__comments {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
font-size: var(--font-small);
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: visible;
|
|
||||||
padding-top: var(--spacing-s);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment {
|
|
||||||
list-style-type: none;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-card-background-highlighted);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:hover) {
|
|
||||||
.menu__button:not(:focus):not([aria-expanded='true']) {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-name {
|
|
||||||
font-size: var(--font-xsmall);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment__info {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment--superchat {
|
|
||||||
background-color: var(--color-card-background-highlighted);
|
|
||||||
|
|
||||||
+ .livestream-comment--superchat {
|
|
||||||
margin-bottom: var(--spacing-xxs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment__body {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: unset;
|
|
||||||
flex: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment__info {
|
|
||||||
margin-top: calc(var(--spacing-xxs) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 4rem;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
width: 5px;
|
|
||||||
background-color: var(--color-superchat);
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment__body {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.livestream-comment__info--sticker {
|
|
||||||
display: flex;
|
|
||||||
margin: var(--spacing-xxs) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-thumbnail {
|
|
||||||
@include handleChannelGif(2rem);
|
|
||||||
margin-top: var(--spacing-xxs);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment__menu {
|
|
||||||
position: absolute;
|
|
||||||
right: var(--spacing-xs);
|
|
||||||
top: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__comments__scroll-to-recent {
|
|
||||||
margin-top: -$recent-msg-button__height;
|
|
||||||
align-self: center;
|
|
||||||
margin-bottom: var(--spacing-xs);
|
|
||||||
font-size: var(--font-xsmall);
|
|
||||||
padding: var(--spacing-xxs) var(--spacing-s);
|
|
||||||
opacity: 0.9;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__comment-create {
|
|
||||||
padding: var(--spacing-s);
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__channel-link {
|
.livestream__channel-link {
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl);
|
||||||
box-shadow: 0 0 0 rgba(246, 72, 83, 0.4);
|
box-shadow: 0 0 0 rgba(246, 72, 83, 0.4);
|
||||||
|
@ -243,63 +87,7 @@ $recent-msg-button__height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream-superchats__wrapper {
|
.livestreamSuperchat {
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
overflow-x: scroll;
|
|
||||||
padding: var(--spacing-s) var(--spacing-xs);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
font-size: var(--font-small);
|
|
||||||
background-color: var(--color-card-background);
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
width: var(--livestream-comments-width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-pinned__wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
padding: var(--spacing-s) var(--spacing-xs);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
font-size: var(--font-small);
|
|
||||||
background-color: var(--color-card-background-highlighted);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.livestream-comment {
|
|
||||||
width: 100%;
|
|
||||||
padding-top: var(--spacing-xs);
|
|
||||||
max-height: 6rem;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
border-left: 1px solid var(--color-border);
|
|
||||||
padding: 0 calc(var(--spacing-m) - var(--spacing-xs)) 0 var(--spacing-m);
|
|
||||||
color: var(--color-text-subtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
padding: var(--spacing-xs);
|
|
||||||
width: var(--livestream-comments-width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat__amount-large {
|
|
||||||
.credit-amount {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchats__inner {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
padding: var(--spacing-xxs);
|
padding: var(--spacing-xxs);
|
||||||
|
@ -337,100 +125,6 @@ $recent-msg-button__height: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream-superchat__info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: var(--font-xsmall);
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat__info--sticker {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 8rem;
|
|
||||||
height: 3rem;
|
|
||||||
|
|
||||||
.livestream-superchat__info--image {
|
|
||||||
padding-left: var(--spacing-m);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
margin-top: calc(var(--spacing-xxs) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat__info--not-sticker {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat__banner {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
padding: 0.25rem var(--spacing-s);
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just a two small circles that overlap to make it look like
|
|
||||||
// the banner and the left border are connected
|
|
||||||
.livestream-superchat__banner-corner {
|
|
||||||
height: calc(var(--border-radius) * 2);
|
|
||||||
width: calc(var(--border-radius) * 2);
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
background-color: var(--color-superchat);
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: translateX(0) translateY(50%);
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
height: calc(var(--border-radius) * 2);
|
|
||||||
width: calc(var(--border-radius) * 2);
|
|
||||||
border-top-left-radius: var(--border-radius);
|
|
||||||
background-color: var(--color-card-background);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
transform: translateX(25%) translateY(50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-comment__text {
|
|
||||||
padding-right: var(--spacing-xl);
|
|
||||||
padding-bottom: var(--spacing-xxs);
|
|
||||||
|
|
||||||
.markdown-preview {
|
|
||||||
p {
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-name {
|
|
||||||
font-size: var(--font-small);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat__tooltip-amount {
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
margin-left: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream__superchat-comment {
|
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
max-width: 5rem;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.livestream-superchat__amount-large {
|
|
||||||
min-width: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table--livestream-data {
|
.table--livestream-data {
|
||||||
td:nth-of-type(1) {
|
td:nth-of-type(1) {
|
||||||
max-width: 4rem;
|
max-width: 4rem;
|
||||||
|
|
|
@ -223,7 +223,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $breakpoint-medium) and (max-width: $breakpoint-large + 300px) {
|
@media (min-width: $breakpoint-medium) and (max-width: $breakpoint-large + 300px) {
|
||||||
max-width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 3);
|
max-width: calc(100vw - var(--livestreamComments-width) - var(--spacing-m) * 3);
|
||||||
margin-left: var(--spacing-m);
|
margin-left: var(--spacing-m);
|
||||||
margin-right: var(--spacing-m);
|
margin-right: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
@ -234,7 +234,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.main__right-side {
|
.main__right-side {
|
||||||
width: var(--livestream-comments-width);
|
width: var(--livestreamComments-width);
|
||||||
|
|
||||||
@media (max-width: $breakpoint-medium) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.super-chat--light {
|
.superChat--light {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline;
|
display: inline;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.super-chat {
|
.superChat {
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background: linear-gradient(to right, var(--color-superchat), var(--color-superchat-3));
|
background: linear-gradient(to right, var(--color-superchat), var(--color-superchat-3));
|
||||||
padding: 0.2rem var(--spacing-xs);
|
padding: 0.2rem var(--spacing-xs);
|
||||||
|
@ -10,8 +10,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.super-chat--light {
|
.superChat--light {
|
||||||
@extend .super-chat;
|
@extend .superChat;
|
||||||
background: linear-gradient(to right, var(--color-superchat__light), var(--color-superchat-3__light));
|
background: linear-gradient(to right, var(--color-superchat__light), var(--color-superchat-3__light));
|
||||||
|
|
||||||
.credit-amount {
|
.credit-amount {
|
||||||
|
|
|
@ -247,7 +247,7 @@ textarea {
|
||||||
margin: -1rem 0;
|
margin: -1rem 0;
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
|
|
||||||
background: url('../../static/img/busy.gif') no-repeat center center;
|
background: url('../../../static/img/busy.gif') no-repeat center center;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ $breakpoint-large: 1600px;
|
||||||
|
|
||||||
--tag-height: 1.5rem;
|
--tag-height: 1.5rem;
|
||||||
|
|
||||||
--livestream-comments-width: 30rem;
|
--livestreamComments-width: 30rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
|
6
ui/util/scale.js
Normal file
6
ui/util/scale.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export function scaleToDevicePixelRatio(value: number) {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
||||||
|
return devicePixelRatio < 1.0 ? Math.ceil(value / devicePixelRatio) : Math.ceil(value * devicePixelRatio);
|
||||||
|
}
|
|
@ -18,7 +18,6 @@
|
||||||
@import '../../ui/scss/component/channel-mention';
|
@import '../../ui/scss/component/channel-mention';
|
||||||
@import '../../ui/scss/component/claim-list';
|
@import '../../ui/scss/component/claim-list';
|
||||||
@import '../../ui/scss/component/collection';
|
@import '../../ui/scss/component/collection';
|
||||||
@import '../../ui/scss/component/comments';
|
|
||||||
@import '../../ui/scss/component/content';
|
@import '../../ui/scss/component/content';
|
||||||
@import '../../ui/scss/component/dat-gui';
|
@import '../../ui/scss/component/dat-gui';
|
||||||
@import '../../ui/scss/component/embed-player';
|
@import '../../ui/scss/component/embed-player';
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
@import '../../ui/scss/component/channel-mention';
|
@import '../../ui/scss/component/channel-mention';
|
||||||
@import '../../ui/scss/component/claim-list';
|
@import '../../ui/scss/component/claim-list';
|
||||||
@import '../../ui/scss/component/collection';
|
@import '../../ui/scss/component/collection';
|
||||||
@import '../../ui/scss/component/comments';
|
|
||||||
@import '../../ui/scss/component/content';
|
@import '../../ui/scss/component/content';
|
||||||
@import '../../ui/scss/component/dat-gui';
|
@import '../../ui/scss/component/dat-gui';
|
||||||
@import '../../ui/scss/component/embed-player';
|
@import '../../ui/scss/component/embed-player';
|
||||||
|
|
|
@ -98,7 +98,7 @@ $breakpoint-large: 1600px;
|
||||||
|
|
||||||
--tag-height: 1.5rem;
|
--tag-height: 1.5rem;
|
||||||
|
|
||||||
--livestream-comments-width: 30rem;
|
--livestreamComments-width: 30rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
|
|
Loading…
Reference in a new issue