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,
|
||||
selectMyChannelClaims,
|
||||
} 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 {
|
||||
selectLinkedCommentAncestors,
|
||||
selectOthersReactsForComment,
|
||||
makeSelectTotalReplyPagesForParentId,
|
||||
} 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 { selectPlayingUri } from 'redux/selectors/content';
|
||||
import Comment from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import CommentView from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { channel_url: authorUri, comment_id: commentId } = props.comment;
|
||||
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||
const reactionKey = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
|
||||
|
||||
return {
|
||||
channelIsBlocked: authorUri && makeSelectChannelIsMuted(authorUri)(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),
|
||||
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 })),
|
||||
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
|
||||
fetchReplies: (uri, parentId, page, pageSize, sortBy) =>
|
||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
||||
updateComment: (editedComment) => dispatch(doCommentUpdate(ownProps.comment.comment_id, editedComment)),
|
||||
fetchReplies: (page, pageSize, sortBy) =>
|
||||
dispatch(doCommentList(ownProps.uri, ownProps.comment.comment_id, page, pageSize, sortBy)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(Comment);
|
||||
export default connect(select, perform)(CommentView);
|
||||
|
|
|
@ -1,120 +1,100 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
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 'scss/component/_comments.scss';
|
||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import { Menu, MenuButton } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import classnames from 'classnames';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
import CommentsReplies from 'component/commentsReplies';
|
||||
import { parseSticker } from 'util/comments';
|
||||
import { parseURI } from 'util/lbryURI';
|
||||
import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
|
||||
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 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 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 { 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;
|
||||
|
||||
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;
|
||||
|
||||
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 {
|
||||
clearPlayingUri,
|
||||
claim,
|
||||
uri,
|
||||
author,
|
||||
authorUri,
|
||||
timePosted,
|
||||
message,
|
||||
channelIsBlocked,
|
||||
commentIsMine,
|
||||
commentId,
|
||||
updateComment,
|
||||
fetchReplies,
|
||||
totalReplyPages,
|
||||
linkedCommentId,
|
||||
linkedCommentAncestors,
|
||||
commentingEnabled,
|
||||
myChannels,
|
||||
doToast,
|
||||
isTopLevel,
|
||||
threadDepth,
|
||||
claim,
|
||||
comment,
|
||||
hideActions,
|
||||
isPinned,
|
||||
isTopLevel,
|
||||
linkedCommentAncestors,
|
||||
linkedCommentId,
|
||||
myChannels,
|
||||
othersReacts,
|
||||
playingUri,
|
||||
stakedLevel,
|
||||
supportAmount,
|
||||
numDirectReplies,
|
||||
isModerator,
|
||||
isGlobalMod,
|
||||
isFiat,
|
||||
supportDisabled,
|
||||
setQuickReply,
|
||||
quickReply,
|
||||
stakedLevel,
|
||||
supportDisabled,
|
||||
threadDepth,
|
||||
totalReplyPages,
|
||||
uri,
|
||||
userCanComment,
|
||||
clearPlayingUri,
|
||||
fetchReplies,
|
||||
setQuickReply,
|
||||
updateComment,
|
||||
} = props;
|
||||
|
||||
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,
|
||||
location: { pathname, search },
|
||||
} = useHistory();
|
||||
|
@ -129,79 +109,22 @@ function Comment(props: Props) {
|
|||
const [isReplying, setReplying] = React.useState(false);
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
const [editedMessage, setCommentValue] = useState(message);
|
||||
const [charCount, setCharCount] = useState(editedMessage.length);
|
||||
const [showReplies, setShowReplies] = useState(showRepliesOnMount);
|
||||
const [page, setPage] = useState(showRepliesOnMount ? 1 : 0);
|
||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
||||
const hasChannels = myChannels && myChannels.length > 0;
|
||||
|
||||
const likesCount = (othersReacts && othersReacts.like) || 0;
|
||||
const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
|
||||
const totalLikesAndDislikes = likesCount + dislikesCount;
|
||||
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);
|
||||
|
||||
let channelOwnerOfContent;
|
||||
let contentOwnerChannel;
|
||||
try {
|
||||
const { channelName } = parseURI(uri);
|
||||
if (channelName) {
|
||||
channelOwnerOfContent = channelName;
|
||||
}
|
||||
({ channelName: contentOwnerChannel } = parseURI(uri));
|
||||
} 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() {
|
||||
const urlParams = new URLSearchParams(search);
|
||||
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 (
|
||||
<li
|
||||
className={classnames('comment', {
|
||||
'comment--top-level': isTopLevel,
|
||||
'comment--topLevel': isTopLevel,
|
||||
'comment--reply': !isTopLevel,
|
||||
'comment--superchat': supportAmount > 0,
|
||||
})}
|
||||
|
@ -233,48 +173,25 @@ function Comment(props: Props) {
|
|||
<div
|
||||
ref={isLinkedComment ? linkedCommentRef : undefined}
|
||||
className={classnames('comment__content', {
|
||||
[COMMENT_HIGHLIGHTED]: isLinkedComment,
|
||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||
'comment__content--highlighted': isLinkedComment,
|
||||
'comment__content--slimed': slimedToDeath && !displayDeadComment,
|
||||
})}
|
||||
>
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" />
|
||||
) : (
|
||||
<ChannelThumbnail xsmall className="comment__author-thumbnail" />
|
||||
)}
|
||||
<div className="commentThumbnail__wrapper">
|
||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="commentAuthor__thumbnail" />
|
||||
</div>
|
||||
|
||||
<div className="comment__body-container">
|
||||
<div className="commentBody__container">
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{isGlobalMod && (
|
||||
<Tooltip label={__('Admin')}>
|
||||
<span className="comment__badge comment__badge--global-mod">
|
||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="commentMeta__information">
|
||||
{isModerator && commentBadge(__('Moderator'), 'commentBadge__mod', ICONS.BADGE_MOD)}
|
||||
{isGlobalMod && commentBadge(__('Admin'), 'commentBadge__globalMod', ICONS.BADGE_MOD)}
|
||||
|
||||
{isModerator && (
|
||||
<Tooltip label={__('Moderator')}>
|
||||
<span className="comment__badge comment__badge--mod">
|
||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!author ? (
|
||||
<span className="comment__author">{__('Anonymous')}</span>
|
||||
) : (
|
||||
<UriIndicator
|
||||
className={classnames('comment__author', {
|
||||
'comment__author--creator': commentByOwnerOfContent,
|
||||
})}
|
||||
link
|
||||
uri={authorUri}
|
||||
className={classnames('comment__author', { 'comment__author--creator': commentByContentOwner })}
|
||||
link
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className="comment__time"
|
||||
onClick={handleTimeClick}
|
||||
|
@ -286,8 +203,8 @@ function Comment(props: Props) {
|
|||
{isPinned && (
|
||||
<span className="comment__pin">
|
||||
<Icon icon={ICONS.PIN} size={14} />
|
||||
{channelOwnerOfContent
|
||||
? __('Pinned by @%channel%', { channel: channelOwnerOfContent })
|
||||
{contentOwnerChannel
|
||||
? __('Pinned by @%channel%', { channel: contentOwnerChannel })
|
||||
: __('Pinned by creator')}
|
||||
</span>
|
||||
)}
|
||||
|
@ -304,63 +221,57 @@ function Comment(props: Props) {
|
|||
commentId={commentId}
|
||||
authorUri={authorUri}
|
||||
commentIsMine={commentIsMine}
|
||||
handleEditComment={handleEditComment}
|
||||
handleEditComment={() => {
|
||||
if (playingUri && playingUri.source === 'comment') clearPlayingUri();
|
||||
setEditing(true);
|
||||
}}
|
||||
supportAmount={supportAmount}
|
||||
setQuickReply={setQuickReply}
|
||||
disableEdit={Boolean(stickerFromMessage)}
|
||||
/>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="comment__body">
|
||||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
className="comment__edit-input"
|
||||
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="editing_comment"
|
||||
value={editedMessage}
|
||||
charCount={charCount}
|
||||
onChange={handleEditMessageChanged}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
<CommentCreate
|
||||
isEdit
|
||||
editedMessage={message}
|
||||
onDoneEditing={(editedMessage) => {
|
||||
if (editedMessage) {
|
||||
updateComment(editedMessage);
|
||||
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
|
||||
setCommentValue(editedMessage);
|
||||
}
|
||||
setEditing(false);
|
||||
}}
|
||||
supportDisabled
|
||||
/>
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<Button
|
||||
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 ? (
|
||||
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead">
|
||||
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} />
|
||||
</div>
|
||||
<>
|
||||
{__('This comment was slimed to death.')}
|
||||
<Icon icon={ICONS.SLIME_ACTIVE} />
|
||||
</>
|
||||
) : stickerFromMessage ? (
|
||||
<div className="sticker__comment">
|
||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||
</div>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<Expandable>
|
||||
<MarkdownPreview
|
||||
content={message}
|
||||
promptLinks
|
||||
parentCommentId={commentId}
|
||||
stakedLevel={stakedLevel}
|
||||
/>
|
||||
</Expandable>
|
||||
) : (
|
||||
<MarkdownWrapper>
|
||||
<MarkdownPreview
|
||||
content={message}
|
||||
promptLinks
|
||||
parentCommentId={commentId}
|
||||
stakedLevel={stakedLevel}
|
||||
promptLinks
|
||||
/>
|
||||
</MarkdownWrapper>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -368,10 +279,10 @@ function Comment(props: Props) {
|
|||
<div className="comment__actions">
|
||||
{threadDepth !== 0 && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Log in to reply')}
|
||||
requiresAuth
|
||||
label={userCanComment ? __('Reply') : __('Log in to reply')}
|
||||
className="comment__action"
|
||||
onClick={handleCommentReply}
|
||||
onClick={() => setReplying(!isReplying)}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
)}
|
||||
|
@ -379,7 +290,8 @@ function Comment(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{numDirectReplies > 0 && !showReplies && (
|
||||
{numDirectReplies > 0 &&
|
||||
(!showReplies ? (
|
||||
<div className="comment__actions">
|
||||
<Button
|
||||
label={
|
||||
|
@ -390,16 +302,12 @@ function Comment(props: Props) {
|
|||
button="link"
|
||||
onClick={() => {
|
||||
setShowReplies(true);
|
||||
if (page === 0) {
|
||||
setPage(1);
|
||||
}
|
||||
if (page === 0) setPage(1);
|
||||
}}
|
||||
icon={ICONS.DOWN}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{numDirectReplies > 0 && showReplies && (
|
||||
) : (
|
||||
<div className="comment__actions">
|
||||
<Button
|
||||
label={__('Hide replies')}
|
||||
|
@ -408,7 +316,7 @@ function Comment(props: Props) {
|
|||
icon={ICONS.UP}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
|
||||
{isReplying && (
|
||||
<CommentCreate
|
||||
|
@ -419,9 +327,7 @@ function Comment(props: Props) {
|
|||
setShowReplies(true);
|
||||
setReplying(false);
|
||||
}}
|
||||
onCancelReplying={() => {
|
||||
setReplying(false);
|
||||
}}
|
||||
onCancelReplying={() => setReplying(false)}
|
||||
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),
|
||||
isFetchingChannels: selectFetchingMyChannels(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) => ({
|
||||
|
|
|
@ -45,7 +45,9 @@ type Props = {
|
|||
channels: ?Array<ChannelClaim>,
|
||||
claim: StreamClaim,
|
||||
claimIsMine: boolean,
|
||||
editedMessage?: string,
|
||||
embed?: boolean,
|
||||
isEdit?: boolean,
|
||||
isFetchingChannels: boolean,
|
||||
isNested: boolean,
|
||||
isReply: boolean,
|
||||
|
@ -60,6 +62,7 @@ type Props = {
|
|||
doToast: ({ message: string }) => void,
|
||||
fetchComment: (commentId: string) => Promise<any>,
|
||||
onCancelReplying?: () => void,
|
||||
onDoneEditing?: (editedMessage?: string) => void,
|
||||
onDoneReplying?: () => void,
|
||||
sendCashTip: (TipParams, UserParams, string, ?string, (any) => void) => string,
|
||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||
|
@ -74,7 +77,9 @@ export function CommentCreate(props: Props) {
|
|||
channels,
|
||||
claim,
|
||||
claimIsMine,
|
||||
editedMessage,
|
||||
embed,
|
||||
isEdit,
|
||||
isFetchingChannels,
|
||||
isNested,
|
||||
isReply,
|
||||
|
@ -91,6 +96,7 @@ export function CommentCreate(props: Props) {
|
|||
onCancelReplying,
|
||||
onDoneReplying,
|
||||
sendCashTip,
|
||||
onDoneEditing,
|
||||
sendTip,
|
||||
setQuickReply,
|
||||
} = props;
|
||||
|
@ -114,7 +120,7 @@ export function CommentCreate(props: Props) {
|
|||
const [selectedSticker, setSelectedSticker] = React.useState();
|
||||
const [tipAmount, setTipAmount] = React.useState(1);
|
||||
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 [stickerSelector, setStickerSelector] = React.useState();
|
||||
const [activeTab, setActiveTab] = React.useState('');
|
||||
|
@ -474,16 +480,18 @@ export function CommentCreate(props: Props) {
|
|||
<FormField
|
||||
disabled={isFetchingChannels}
|
||||
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}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
className={(isReply && 'content_reply') || (isEdit && 'commentEdit__input') || 'content_comment'}
|
||||
label={
|
||||
!isEdit && (
|
||||
<span className="commentCreate__labelWrapper">
|
||||
{!livestream && (
|
||||
<div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div>
|
||||
)}
|
||||
<SelectChannel tiny />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
quickActionLabel={
|
||||
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
||||
|
@ -577,16 +585,17 @@ export function CommentCreate(props: Props) {
|
|||
disabled={disabled || stickerSelector}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
(isEdit && __('Done')) ||
|
||||
(isSubmitting
|
||||
? (isReply && __('Replying...')) || __('Commenting...')
|
||||
: (isReply && __('Reply')) || __('Comment --[button to submit something]--'))
|
||||
}
|
||||
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 ||
|
||||
stickerSelector ||
|
||||
isReviewingStickerComment ||
|
||||
(isReply && !minTip)) && (
|
||||
(isReply && !minTip) ||
|
||||
isEdit) && (
|
||||
<Button
|
||||
disabled={isSupportComment && isSubmitting}
|
||||
button="link"
|
||||
|
@ -648,6 +658,8 @@ export function CommentCreate(props: Props) {
|
|||
setStickerSelector(false);
|
||||
} else if (isReply && !minTip && onCancelReplying) {
|
||||
onCancelReplying();
|
||||
} else if (isEdit && onDoneEditing) {
|
||||
onDoneEditing();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
selectFetchingMyChannels,
|
||||
selectMyChannelClaims,
|
||||
} from 'redux/selectors/claims';
|
||||
import {
|
||||
selectTopLevelCommentsForUri,
|
||||
makeSelectTopLevelTotalPagesForUri,
|
||||
|
@ -20,6 +13,8 @@ import {
|
|||
selectPinnedCommentsForUri,
|
||||
} from 'redux/selectors/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 CommentsList from './view';
|
||||
|
||||
|
@ -33,32 +28,31 @@ const select = (state, props) => {
|
|||
: [];
|
||||
|
||||
return {
|
||||
topLevelComments,
|
||||
resolvedComments,
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||
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),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
isFetchingCommentsById: selectIsFetchingCommentsById(state),
|
||||
isFetchingReacts: selectIsFetchingReacts(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
settingsByChannelId: selectSettingsByChannelId(state),
|
||||
myReactsByCommentId: selectMyReacts(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) => ({
|
||||
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
||||
fetchTopLevelComments: (page, pageSize, sortBy) => dispatch(doCommentList(ownProps.uri, '', page, pageSize, sortBy)),
|
||||
resetComments: () => ownProps.claim && dispatch(doCommentReset(ownProps.claim.claimId)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentsList);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// @flow
|
||||
import 'scss/component/_comments-list.scss';
|
||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as REACTION_TYPES from 'constants/reactions';
|
||||
|
@ -18,67 +20,57 @@ import usePersistedState from 'effects/use-persisted-state';
|
|||
|
||||
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 = {
|
||||
activeChannelId: ?string,
|
||||
allCommentIds: any,
|
||||
pinnedComments: Array<Comment>,
|
||||
topLevelComments: Array<Comment>,
|
||||
resolvedComments: Array<Comment>,
|
||||
topLevelTotalPages: number,
|
||||
uri: string,
|
||||
claim: ?Claim,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
commentsAreExpanded?: boolean,
|
||||
fetchingChannels: boolean,
|
||||
isFetchingComments: boolean,
|
||||
isFetchingCommentsById: boolean,
|
||||
isFetchingReacts: boolean,
|
||||
linkedCommentId?: string,
|
||||
totalComments: number,
|
||||
fetchingChannels: boolean,
|
||||
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
|
||||
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||
activeChannelId: ?string,
|
||||
pinnedComments: Array<Comment>,
|
||||
resolvedComments: Array<Comment>,
|
||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||
commentsAreExpanded?: boolean,
|
||||
fetchReacts: (Array<string>) => Promise<any>,
|
||||
topLevelComments: Array<Comment>,
|
||||
topLevelTotalPages: number,
|
||||
totalComments: number,
|
||||
uri: string,
|
||||
doResolveUris: (Array<string>) => void,
|
||||
fetchTopLevelComments: (string, number, number, number) => 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 {
|
||||
activeChannelId,
|
||||
allCommentIds,
|
||||
uri,
|
||||
pinnedComments,
|
||||
topLevelComments,
|
||||
resolvedComments,
|
||||
topLevelTotalPages,
|
||||
claim,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
commentsAreExpanded,
|
||||
doResolveUris,
|
||||
fetchingChannels,
|
||||
fetchReacts,
|
||||
isFetchingComments,
|
||||
isFetchingReacts,
|
||||
linkedCommentId,
|
||||
totalComments,
|
||||
fetchingChannels,
|
||||
myReactsByCommentId,
|
||||
othersReactsById,
|
||||
activeChannelId,
|
||||
pinnedComments,
|
||||
resolvedComments,
|
||||
settingsByChannelId,
|
||||
commentsAreExpanded,
|
||||
fetchReacts,
|
||||
doResolveUris,
|
||||
fetchTopLevelComments,
|
||||
topLevelComments,
|
||||
topLevelTotalPages,
|
||||
totalComments,
|
||||
uri,
|
||||
fetchComment,
|
||||
fetchTopLevelComments,
|
||||
resetComments,
|
||||
} = props;
|
||||
|
||||
|
@ -92,6 +84,7 @@ function CommentList(props: Props) {
|
|||
const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
|
||||
const hasDefaultExpansion = commentsAreExpanded || desktopView;
|
||||
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
|
||||
|
||||
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
|
@ -106,7 +99,7 @@ function CommentList(props: Props) {
|
|||
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
||||
);
|
||||
|
||||
function changeSort(newSort) {
|
||||
function changeSort(newSort: string) {
|
||||
if (sort !== newSort) {
|
||||
setSort(newSort);
|
||||
setPage(0); // Invalidate existing comments
|
||||
|
@ -116,9 +109,7 @@ function CommentList(props: Props) {
|
|||
// Reset comments
|
||||
useEffect(() => {
|
||||
if (page === 0) {
|
||||
if (claim) {
|
||||
resetComments(claim.claim_id);
|
||||
}
|
||||
if (claim) resetComments();
|
||||
setPage(1);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -131,7 +122,7 @@ function CommentList(props: Props) {
|
|||
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]);
|
||||
|
||||
|
@ -237,39 +228,25 @@ function CommentList(props: Props) {
|
|||
if (!topLevelComments || alreadyResolved) return;
|
||||
|
||||
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);
|
||||
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||
|
||||
const getCommentElems = (comments) =>
|
||||
comments.map((comment) => (
|
||||
const getCommentElems = (comments: Array<Comment>) =>
|
||||
comments.map((comment: Comment) => (
|
||||
<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}
|
||||
commentIsMine={
|
||||
comment.channel_id && myChannels && myChannels.some(({ claim_id }) => claim_id === comment.channel_id)
|
||||
}
|
||||
comment={comment}
|
||||
isTopLevel
|
||||
key={comment.comment_id}
|
||||
linkedCommentId={linkedCommentId}
|
||||
isPinned={comment.is_pinned}
|
||||
supportAmount={comment.support_amount}
|
||||
numDirectReplies={comment.replies}
|
||||
isModerator={comment.is_moderator}
|
||||
isGlobalMod={comment.is_global_mod}
|
||||
isFiat={comment.is_fiat}
|
||||
threadDepth={3}
|
||||
uri={uri}
|
||||
/>
|
||||
));
|
||||
|
||||
const sortButton = (label, icon, sortOption) => (
|
||||
const sortButton = (label: string, icon: string, sortOption: string) => (
|
||||
<Button
|
||||
button="alt"
|
||||
label={label}
|
||||
|
@ -311,8 +288,7 @@ function CommentList(props: Props) {
|
|||
)}
|
||||
|
||||
<ul
|
||||
className={classnames({
|
||||
comments: desktopView || expandedComments,
|
||||
className={classnames('comments', {
|
||||
'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 { 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 { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import CommentsReplies from './view';
|
||||
|
@ -13,12 +13,11 @@ const select = (state, props) => {
|
|||
: [];
|
||||
|
||||
return {
|
||||
fetchedReplies,
|
||||
resolvedReplies,
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
userCanComment: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
fetchedReplies,
|
||||
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
||||
resolvedReplies,
|
||||
userCanComment: selectUserVerifiedEmail(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import 'scss/component/_comments.scss';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import Comment from 'component/comment';
|
||||
|
@ -6,44 +7,43 @@ import React from 'react';
|
|||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
fetchedReplies: Array<Comment>,
|
||||
resolvedReplies: Array<Comment>,
|
||||
uri: string,
|
||||
parentId: string,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
linkedCommentId?: string,
|
||||
userCanComment: boolean,
|
||||
threadDepth: number,
|
||||
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
|
||||
isFetchingByParentId: { [string]: boolean },
|
||||
fetchedReplies: Array<Comment>,
|
||||
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,
|
||||
threadDepth: number,
|
||||
uri: string,
|
||||
userCanComment: boolean,
|
||||
doResolveUris: (Array<string>) => void,
|
||||
onShowMore?: () => void,
|
||||
};
|
||||
|
||||
function CommentsReplies(props: Props) {
|
||||
const {
|
||||
uri,
|
||||
parentId,
|
||||
fetchedReplies,
|
||||
resolvedReplies,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
linkedCommentId,
|
||||
userCanComment,
|
||||
threadDepth,
|
||||
numDirectReplies,
|
||||
isFetchingByParentId,
|
||||
fetchedReplies,
|
||||
hasMore,
|
||||
isFetchingByParentId,
|
||||
linkedCommentId,
|
||||
numDirectReplies,
|
||||
parentId,
|
||||
resolvedReplies,
|
||||
supportDisabled,
|
||||
threadDepth,
|
||||
uri,
|
||||
userCanComment,
|
||||
doResolveUris,
|
||||
onShowMore,
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setExpanded] = React.useState(true);
|
||||
const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies);
|
||||
|
||||
const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length;
|
||||
const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0;
|
||||
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length;
|
||||
|
@ -53,7 +53,7 @@ function CommentsReplies(props: Props) {
|
|||
if (!fetchedReplies || alreadyResolved) return;
|
||||
|
||||
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);
|
||||
}, [alreadyResolved, doResolveUris, fetchedReplies]);
|
||||
|
@ -65,7 +65,7 @@ function CommentsReplies(props: Props) {
|
|||
}, [isResolvingReplies, fetchedReplies]);
|
||||
|
||||
return !numDirectReplies ? null : (
|
||||
<div className="comment__replies-container">
|
||||
<div className="commentReplies__container">
|
||||
{!isExpanded ? (
|
||||
<div className="comment__actions--nested">
|
||||
<Button
|
||||
|
@ -83,35 +83,22 @@ function CommentsReplies(props: Props) {
|
|||
{!isResolvingReplies &&
|
||||
commentsToDisplay &&
|
||||
commentsToDisplay.length > 0 &&
|
||||
commentsToDisplay.map((comment) => (
|
||||
commentsToDisplay.map((comment: Comment) => (
|
||||
<Comment
|
||||
claimIsMine={claimIsMine}
|
||||
comment={comment}
|
||||
commentingEnabled={userCanComment}
|
||||
key={comment.comment_id}
|
||||
linkedCommentId={linkedCommentId}
|
||||
supportDisabled={supportDisabled}
|
||||
threadDepth={threadDepth}
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isExpanded && fetchedReplies && hasMore && (
|
||||
<div className="comment__actions--nested">
|
||||
<Button
|
||||
|
@ -122,12 +109,11 @@ function CommentsReplies(props: Props) {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && (
|
||||
<div className="comment__replies-container">
|
||||
<div className="comment__actions--nested">
|
||||
<Spinner type="small" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -92,8 +92,8 @@ class CreditAmount extends React.PureComponent<Props> {
|
|||
<span
|
||||
title={fullPrice}
|
||||
className={classnames(className, {
|
||||
'super-chat': superChat,
|
||||
'super-chat--light': superChatLight,
|
||||
superChat: superChat,
|
||||
'superChat--light': superChatLight,
|
||||
})}
|
||||
>
|
||||
<span className="credit-amount">{amountText}</span>
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||
import debounce from 'util/debounce';
|
||||
import React from 'react';
|
||||
|
||||
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 = {
|
||||
children: any,
|
||||
skipWait?: boolean,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectStakedLevelForChannelUri, selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { makeSelectStakedLevelForChannelUri, selectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims';
|
||||
import LivestreamComment from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: selectClaimForUri(state, props.uri),
|
||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
});
|
||||
|
||||
export default connect(select)(LivestreamComment);
|
||||
|
|
|
@ -1,102 +1,87 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
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 'scss/component/_livestream-comment.scss';
|
||||
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 CommentMenuList from 'component/commentMenuList';
|
||||
import Button from 'component/button';
|
||||
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 { parseSticker } from 'util/comments';
|
||||
import React from 'react';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: StreamClaim,
|
||||
authorUri: string,
|
||||
commentId: string,
|
||||
message: string,
|
||||
commentIsMine: boolean,
|
||||
comment: Comment,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
stakedLevel: number,
|
||||
supportAmount: number,
|
||||
isModerator: boolean,
|
||||
isGlobalMod: boolean,
|
||||
isFiat: boolean,
|
||||
isPinned: boolean,
|
||||
uri: string,
|
||||
};
|
||||
|
||||
function LivestreamComment(props: Props) {
|
||||
const {
|
||||
claim,
|
||||
uri,
|
||||
authorUri,
|
||||
message,
|
||||
commentIsMine,
|
||||
commentId,
|
||||
stakedLevel,
|
||||
supportAmount,
|
||||
isModerator,
|
||||
isGlobalMod,
|
||||
isFiat,
|
||||
isPinned,
|
||||
} = props;
|
||||
const { claim, comment, myChannels, stakedLevel, uri } = props;
|
||||
|
||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||
const { claimName } = parseURI(authorUri);
|
||||
const {
|
||||
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);
|
||||
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 (
|
||||
<li
|
||||
className={classnames('livestream-comment', {
|
||||
'livestream-comment--superchat': supportAmount > 0,
|
||||
'livestream-comment--sticker': Boolean(stickerFromMessage),
|
||||
className={classnames('livestreamComment', {
|
||||
'livestreamComment--superchat': supportAmount > 0,
|
||||
'livestreamComment--sticker': Boolean(stickerFromMessage),
|
||||
})}
|
||||
>
|
||||
{supportAmount > 0 && (
|
||||
<div className="super-chat livestream-superchat__banner">
|
||||
<div className="livestream-superchat__banner-corner" />
|
||||
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestream-superchat__amount" />
|
||||
<div className="superChat livestreamComment__superchatBanner">
|
||||
<div className="livestreamComment__superchatBanner-corner" />
|
||||
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestreamSuperchat__amount" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="livestream-comment__body">
|
||||
<div className="livestreamComment__body">
|
||||
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||
<div
|
||||
className={classnames('livestream-comment__info', {
|
||||
'livestream-comment__info--sticker': Boolean(stickerFromMessage),
|
||||
className={classnames('livestreamComment__info', {
|
||||
'livestreamComment__info--sticker': Boolean(stickerFromMessage),
|
||||
})}
|
||||
>
|
||||
{isGlobalMod && (
|
||||
<Tooltip label={__('Admin')}>
|
||||
<span className="comment__badge comment__badge--global-mod">
|
||||
<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>
|
||||
)}
|
||||
{isGlobalMod && commentBadge(__('Admin'), ICONS.BADGE_MOD, 'comment__badge--globalMod')}
|
||||
{isModerator && commentBadge(__('Moderator'), ICONS.BADGE_MOD, 'comment__badge--mod')}
|
||||
{commentByContentOwner && commentBadge(__('Streamer'), ICONS.BADGE_STREAMER)}
|
||||
|
||||
<Button
|
||||
className={classnames('button--uri-indicator comment__author', {
|
||||
'comment__author--creator': commentByOwnerOfContent,
|
||||
'comment__author--creator': commentByContentOwner,
|
||||
})}
|
||||
target="_blank"
|
||||
navigate={authorUri}
|
||||
|
@ -112,18 +97,18 @@ function LivestreamComment(props: Props) {
|
|||
)}
|
||||
|
||||
{stickerFromMessage ? (
|
||||
<div className="sticker__comment">
|
||||
<div className="comment__message--sticker">
|
||||
<OptimizedImage src={stickerFromMessage.url} waitLoad />
|
||||
</div>
|
||||
) : (
|
||||
<div className="livestream-comment__text">
|
||||
<div className="livestreamComment__text">
|
||||
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="livestream-comment__menu">
|
||||
<div className="livestreamComment__menu">
|
||||
<Menu>
|
||||
<MenuButton className="menu__button">
|
||||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||
import {
|
||||
|
@ -20,7 +20,6 @@ const select = (state, props) => ({
|
|||
fetchingComments: selectIsFetchingComments(state),
|
||||
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
||||
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
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 'scss/component/_livestream-comments.scss';
|
||||
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 = {
|
||||
uri: string,
|
||||
claim: ?StreamClaim,
|
||||
activeViewers: number,
|
||||
claim: ?StreamClaim,
|
||||
comments: Array<Comment>,
|
||||
embed?: boolean,
|
||||
fetchingComments: boolean,
|
||||
pinnedComments: Array<Comment>,
|
||||
superChats: Array<Comment>,
|
||||
uri: string,
|
||||
doCommentList: (string, string, number, number) => void,
|
||||
doCommentSocketConnect: (string, string) => void,
|
||||
doCommentSocketDisconnect: (string) => void,
|
||||
doCommentList: (string, string, number, number) => void,
|
||||
comments: Array<Comment>,
|
||||
pinnedComments: Array<Comment>,
|
||||
fetchingComments: boolean,
|
||||
doSuperChatList: (string) => void,
|
||||
superChats: Array<Comment>,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
};
|
||||
|
||||
const VIEW_MODE_CHAT = 'view_chat';
|
||||
|
@ -36,17 +36,16 @@ const COMMENT_SCROLL_TIMEOUT = 25;
|
|||
export default function LivestreamComments(props: Props) {
|
||||
const {
|
||||
claim,
|
||||
uri,
|
||||
comments: commentsByChronologicalOrder,
|
||||
embed,
|
||||
fetchingComments,
|
||||
pinnedComments,
|
||||
superChats: superChatsByTipAmount,
|
||||
uri,
|
||||
doCommentList,
|
||||
doCommentSocketConnect,
|
||||
doCommentSocketDisconnect,
|
||||
comments: commentsByChronologicalOrder,
|
||||
pinnedComments,
|
||||
doCommentList,
|
||||
fetchingComments,
|
||||
doSuperChatList,
|
||||
myChannels,
|
||||
superChats: superChatsByTipAmount,
|
||||
} = props;
|
||||
|
||||
let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats;
|
||||
|
@ -55,22 +54,28 @@ export default function LivestreamComments(props: Props) {
|
|||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||
const [scrollPos, setScrollPos] = React.useState(0);
|
||||
const [showPinned, setShowPinned] = React.useState(true);
|
||||
|
||||
const claimId = claim && claim.claim_id;
|
||||
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
|
||||
|
||||
// which kind of superchat to display, either
|
||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
||||
const stickerSuperChats =
|
||||
superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment)));
|
||||
|
||||
const discussionElement = document.querySelector('.livestream__comments');
|
||||
|
||||
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
|
||||
|
||||
const restoreScrollPos = React.useCallback(() => {
|
||||
if (discussionElement) {
|
||||
discussionElement.scrollTop = 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) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
const restoreScrollPos = React.useCallback(() => {
|
||||
if (discussionElement) discussionElement.scrollTop = 0;
|
||||
}, [discussionElement]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -139,75 +144,41 @@ export default function LivestreamComments(props: Props) {
|
|||
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) {
|
||||
const stickerFromComment = parseSticker(comment);
|
||||
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__header--between livestream-discussion__header">
|
||||
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
||||
<div className="card__header--between livestreamDiscussion__header">
|
||||
<div className="card__title-section--small livestreamDiscussion__title">{__('Live discussion')}</div>
|
||||
{hasSuperChats && (
|
||||
<div className="recommended-content__toggles">
|
||||
{/* the superchats in chronological order button */}
|
||||
<Button
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
{getChatContentToggle(VIEW_MODE_CHAT, __('Chat'))}
|
||||
|
||||
{/* the button to show superchats listed by most to least support amount */}
|
||||
<Button
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT,
|
||||
})}
|
||||
label={
|
||||
{getChatContentToggle(
|
||||
VIEW_MODE_SUPER_CHAT,
|
||||
<>
|
||||
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
|
||||
<CreditAmount amount={superChatsTotalAmount || 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>
|
||||
|
@ -217,10 +188,10 @@ export default function LivestreamComments(props: Props) {
|
|||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
||||
<div ref={commentsRef} className="livestreamComments__wrapper">
|
||||
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && (
|
||||
<div className="livestream-superchats__wrapper">
|
||||
<div className="livestream-superchats__inner">
|
||||
<div className="livestreamSuperchats__wrapper">
|
||||
<div className="livestreamSuperchats__inner">
|
||||
{superChatsByTipAmount.map((superChat: Comment) => {
|
||||
const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
|
||||
|
||||
|
@ -230,28 +201,28 @@ export default function LivestreamComments(props: Props) {
|
|||
|
||||
return (
|
||||
<SuperChatWrapper key={superChat.comment_id}>
|
||||
<div className="livestream-superchat">
|
||||
<div className="livestream-superchat__thumbnail">
|
||||
<div className="livestreamSuperchat">
|
||||
<div className="livestreamSuperchat__thumbnail">
|
||||
<ChannelThumbnail uri={superChat.channel_url} xsmall />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames('livestream-superchat__info', {
|
||||
'livestream-superchat__info--sticker': isSticker,
|
||||
'livestream-superchat__info--not-sticker': stickerSuperChats && !isSticker,
|
||||
className={classnames('livestreamSuperchat__info', {
|
||||
'livestreamSuperchat__info--sticker': isSticker,
|
||||
'livestreamSuperchat__info--notSticker': stickerSuperChats && !isSticker,
|
||||
})}
|
||||
>
|
||||
<div className="livestream-superchat__info--user">
|
||||
<div className="livestreamSuperchat__info--user">
|
||||
<UriIndicator uri={superChat.channel_url} link />
|
||||
<CreditAmount
|
||||
size={10}
|
||||
className="livestream-superchat__amount-large"
|
||||
className="livestreamSuperchat__amount-large"
|
||||
amount={superChat.support_amount}
|
||||
isFiat={superChat.is_fiat}
|
||||
/>
|
||||
</div>
|
||||
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
|
||||
<div className="livestream-superchat__info--image">
|
||||
<div className="livestreamSuperchat__info--image">
|
||||
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad />
|
||||
</div>
|
||||
)}
|
||||
|
@ -265,20 +236,8 @@ export default function LivestreamComments(props: Props) {
|
|||
)}
|
||||
|
||||
{pinnedComment && showPinned && viewMode === VIEW_MODE_CHAT && (
|
||||
<div className="livestream-pinned__wrapper">
|
||||
<LivestreamComment
|
||||
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)}
|
||||
/>
|
||||
<div className="livestreamPinned__wrapper">
|
||||
<LivestreamComment key={pinnedComment.comment_id} uri={uri} comment={pinnedComment} />
|
||||
<Button
|
||||
title={__('Dismiss pinned comment')}
|
||||
button="inverse"
|
||||
|
@ -294,36 +253,14 @@ export default function LivestreamComments(props: Props) {
|
|||
<div className="livestream__comments">
|
||||
{viewMode === VIEW_MODE_CHAT &&
|
||||
commentsToDisplay.map((comment) => (
|
||||
<LivestreamComment
|
||||
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)}
|
||||
/>
|
||||
<LivestreamComment key={comment.comment_id} uri={uri} comment={comment} />
|
||||
))}
|
||||
|
||||
{/* listing comments on top of eachother */}
|
||||
{viewMode === VIEW_MODE_SUPER_CHAT &&
|
||||
superChatsReversed &&
|
||||
superChatsReversed.map((comment) => (
|
||||
<LivestreamComment
|
||||
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)}
|
||||
/>
|
||||
<LivestreamComment key={comment.comment_id} uri={uri} comment={comment} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
@ -333,14 +270,14 @@ export default function LivestreamComments(props: Props) {
|
|||
{scrollPos < 0 && viewMode === VIEW_MODE_CHAT && (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="livestream__comments__scroll-to-recent"
|
||||
className="livestreamComments__scrollToRecent"
|
||||
label={__('Recent Comments')}
|
||||
onClick={restoreScrollPos}
|
||||
iconRight={ICONS.DOWN}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="livestream__comment-create">
|
||||
<div className="livestream__commentCreate">
|
||||
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||
|
||||
function scaleToDevicePixelRatio(value: number, window: any) {
|
||||
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
||||
return Math.ceil(value * devicePixelRatio);
|
||||
}
|
||||
import { scaleToDevicePixelRatio } from 'util/scale';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
src: string,
|
||||
|
@ -40,8 +36,8 @@ function OptimizedImage(props: Props) {
|
|||
let width = elem.parentElement.clientWidth;
|
||||
let height = elem.parentElement.clientHeight;
|
||||
|
||||
width = scaleToDevicePixelRatio(width, window);
|
||||
height = scaleToDevicePixelRatio(height, window);
|
||||
width = scaleToDevicePixelRatio(width);
|
||||
height = scaleToDevicePixelRatio(height);
|
||||
|
||||
// Round to next 100px for better caching
|
||||
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 { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
import { doClaimSearch } from 'redux/actions/claims';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { doUserSetReferrer } from 'redux/actions/user';
|
||||
import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards';
|
||||
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
|
||||
import LivestreamPage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
channelClaim: makeSelectClaimForUri(props.uri)(state),
|
||||
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doSetPlayingUri,
|
||||
doResolveUri,
|
||||
doUserSetReferrer,
|
||||
})(LivestreamPage);
|
||||
const perform = (dispatch) => ({
|
||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
||||
doClaimSearch: (options, cb) => dispatch(doClaimSearch(options, cb)),
|
||||
setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(LivestreamPage);
|
||||
|
|
|
@ -1,89 +1,81 @@
|
|||
// @flow
|
||||
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 Lbry from 'lbry';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: StreamClaim,
|
||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||
isAuthenticated: boolean,
|
||||
doUserSetReferrer: (string) => void,
|
||||
channelClaim: ChannelClaim,
|
||||
chatDisabled: boolean,
|
||||
};
|
||||
|
||||
export default function LivestreamPage(props: Props) {
|
||||
const { uri, claim, doSetPlayingUri, isAuthenticated, doUserSetReferrer, channelClaim, chatDisabled } = props;
|
||||
const [isLive, setIsLive] = React.useState(false);
|
||||
const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
|
||||
const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false);
|
||||
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 = {
|
||||
channelClaim: ChannelClaim,
|
||||
chatDisabled: boolean,
|
||||
claim: StreamClaim,
|
||||
isAuthenticated: boolean,
|
||||
uri: string,
|
||||
clearPlayingUri: () => void,
|
||||
doClaimSearch: (any, (ClaimSearchResponse) => void) => void,
|
||||
setReferrer: (string) => void,
|
||||
};
|
||||
|
||||
export default function LivestreamPage(props: Props) {
|
||||
const {
|
||||
channelClaim,
|
||||
chatDisabled,
|
||||
claim,
|
||||
isAuthenticated,
|
||||
uri,
|
||||
clearPlayingUri,
|
||||
doClaimSearch,
|
||||
setReferrer,
|
||||
} = props;
|
||||
|
||||
const [isLive, setIsLive] = React.useState(false);
|
||||
const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false);
|
||||
|
||||
const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
|
||||
const stringifiedClaim = JSON.stringify(claim);
|
||||
|
||||
React.useEffect(() => {
|
||||
let checkClaimsInterval;
|
||||
|
||||
function checkHasLivestreamClaim() {
|
||||
Lbry.claim_search({
|
||||
channel_ids: [livestreamChannelId],
|
||||
has_no_source: true,
|
||||
claim_type: ['stream'],
|
||||
})
|
||||
.then((res) => {
|
||||
if (res && res.items && res.items.length > 0) {
|
||||
setHasLivestreamClaim(true);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
doClaimSearch({ channel_ids: [livestreamChannelId], has_no_source: true, claim_type: ['stream'] }, (data) =>
|
||||
data && data.items && data.items.length > 0 ? setHasLivestreamClaim(true) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
if (livestreamChannelId && !isLive) {
|
||||
if (!checkClaimsInterval) checkHasLivestreamClaim();
|
||||
checkClaimsInterval = setInterval(checkHasLivestreamClaim, LIVESTREAM_CLAIM_POLL_IN_MS);
|
||||
|
||||
return () => {
|
||||
if (checkClaimsInterval) {
|
||||
clearInterval(checkClaimsInterval);
|
||||
return () => (checkClaimsInterval ? clearInterval(checkClaimsInterval) : undefined);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [livestreamChannelId, isLive]);
|
||||
}, [livestreamChannelId, isLive, doClaimSearch]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let interval;
|
||||
|
||||
function checkIsLive() {
|
||||
// TODO: duplicate code below
|
||||
// $FlowFixMe livestream API can handle garbage
|
||||
fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (!res || !res.data) {
|
||||
setIsLive(false);
|
||||
return;
|
||||
.then((res) =>
|
||||
!res || !res.data ? setIsLive(false) : res.data.hasOwnProperty('live') && setIsLive(res.data.live)
|
||||
);
|
||||
}
|
||||
|
||||
if (res.data.hasOwnProperty('live')) {
|
||||
setIsLive(res.data.live);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (livestreamChannelId && hasLivestreamClaim) {
|
||||
if (!interval) checkIsLive();
|
||||
interval = setInterval(checkIsLive, STREAMING_POLL_INTERVAL_IN_MS);
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
return () => (interval ? clearInterval(interval) : undefined);
|
||||
}
|
||||
}, [livestreamChannelId, hasLivestreamClaim]);
|
||||
|
||||
const stringifiedClaim = JSON.stringify(claim);
|
||||
React.useEffect(() => {
|
||||
if (uri && stringifiedClaim) {
|
||||
const jsonClaim = JSON.parse(stringifiedClaim);
|
||||
|
@ -97,18 +89,16 @@ export default function LivestreamPage(props: Props) {
|
|||
|
||||
if (!isAuthenticated) {
|
||||
const uri = jsonClaim.signing_channel && jsonClaim.signing_channel.permanent_url;
|
||||
if (uri) {
|
||||
doUserSetReferrer(uri.replace('lbry://', ''));
|
||||
if (uri) setReferrer(uri.replace('lbry://', ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [uri, stringifiedClaim, isAuthenticated]);
|
||||
}, [uri, stringifiedClaim, isAuthenticated, setReferrer]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Set playing uri to null so the popout player doesnt start playing the dummy claim if a user navigates back
|
||||
// This can be removed when we start using the app video player, not a LIVESTREAM iframe
|
||||
doSetPlayingUri({ uri: null });
|
||||
}, [doSetPlayingUri]);
|
||||
clearPlayingUri();
|
||||
}, [clearPlayingUri]);
|
||||
|
||||
return (
|
||||
<Page
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import {
|
||||
selectIsFetchingComments,
|
||||
selectCommentsForUri,
|
||||
makeSelectTotalCommentsCountForUri,
|
||||
makeSelectTopLevelTotalPagesForUri,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectClaimsById } from 'redux/selectors/claims';
|
||||
|
||||
import OwnComments from './view';
|
||||
|
||||
const select = (state) => {
|
||||
|
@ -18,16 +17,16 @@ const select = (state) => {
|
|||
return {
|
||||
activeChannelClaim,
|
||||
allComments: selectCommentsForUri(state, uri),
|
||||
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
|
||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
claimsById: selectClaimsById(state),
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
||||
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doCommentReset: (a) => dispatch(doCommentReset(a)),
|
||||
doCommentListOwn: (a, b, c) => dispatch(doCommentListOwn(a, b, c)),
|
||||
doCommentListOwn: (channelId, page, pageSize) => dispatch(doCommentListOwn(channelId, page, pageSize)),
|
||||
doCommentReset: (claimId) => dispatch(doCommentReset(claimId)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(OwnComments);
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
// @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 Card from 'component/common/card';
|
||||
import ChannelSelector from 'component/channelSelector';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import Comment from 'component/comment';
|
||||
import Card from 'component/common/card';
|
||||
import debounce from 'util/debounce';
|
||||
import Empty from 'component/common/empty';
|
||||
import Page from 'component/page';
|
||||
import React from 'react';
|
||||
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 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 = {
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
|
@ -42,6 +37,7 @@ export default function OwnComments(props: Props) {
|
|||
doCommentReset,
|
||||
doCommentListOwn,
|
||||
} = props;
|
||||
|
||||
const spinnerRef = React.useRef();
|
||||
const [page, setPage] = React.useState(0);
|
||||
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 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
|
||||
React.useEffect(() => {
|
||||
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
|
||||
|
@ -166,34 +117,49 @@ export default function OwnComments(props: Props) {
|
|||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
if (!activeChannelClaim) {
|
||||
return null;
|
||||
}
|
||||
const getCommentsElem = (comments: Array<Comment>) =>
|
||||
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 (
|
||||
<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') }}>
|
||||
<ChannelSelector hideAnon />
|
||||
<Card
|
||||
isBodyList
|
||||
title={
|
||||
totalComments > 0
|
||||
? totalComments === 1
|
||||
? __('1 comment')
|
||||
: __('%total_comments% comments', { total_comments: totalComments })
|
||||
: isFetchingComments
|
||||
? ''
|
||||
: __('No comments')
|
||||
}
|
||||
titleActions={
|
||||
<Button
|
||||
button="alt"
|
||||
icon={ICONS.REFRESH}
|
||||
title={__('Refresh')}
|
||||
onClick={() => {
|
||||
setPage(0);
|
||||
}}
|
||||
/>
|
||||
(isFetchingComments && '') ||
|
||||
(totalComments > 0 && __('No comments')) ||
|
||||
(totalComments === 1 && __('1 comment')) ||
|
||||
(totalComments > 1 && __('%total_comments% comments', { total_comments: totalComments }))
|
||||
}
|
||||
titleActions={<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />}
|
||||
body={
|
||||
<>
|
||||
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}
|
||||
|
|
|
@ -24,6 +24,21 @@ import { doFetchItemsInCollections } from 'redux/actions/collections';
|
|||
let onChannelConfirmCallback;
|
||||
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(
|
||||
uris: Array<string>,
|
||||
returnCachedClaims: boolean = false,
|
||||
|
@ -604,24 +619,8 @@ export function doFetchCollectionListMine(page: number = 1, pageSize: number = 9
|
|||
}
|
||||
|
||||
export function doClaimSearch(
|
||||
options: {
|
||||
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,
|
||||
} = {
|
||||
no_totals: true,
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
}
|
||||
options: ClaimSearchParams = { page_size: 10, page: 1, no_totals: true },
|
||||
successCb?: (ClaimSearchResponse) => void
|
||||
) {
|
||||
const query = createNormalizedClaimSearchKey(options);
|
||||
return async (dispatch: Dispatch) => {
|
||||
|
@ -648,6 +647,9 @@ export function doClaimSearch(
|
|||
pageSize: options.page_size,
|
||||
},
|
||||
});
|
||||
|
||||
if (successCb) successCb(data);
|
||||
|
||||
return resolveInfo;
|
||||
};
|
||||
|
||||
|
|
|
@ -758,9 +758,8 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
|
|||
|
||||
export function doCommentUpdate(comment_id: string, comment: string) {
|
||||
// if they provided an empty string, they must have wanted to abandon
|
||||
if (comment === '') {
|
||||
return doCommentAbandon(comment_id);
|
||||
} else {
|
||||
if (comment === '') return doCommentAbandon(comment_id);
|
||||
|
||||
return async (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
|
||||
|
@ -774,9 +773,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
|||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_UPDATE_STARTED,
|
||||
});
|
||||
dispatch({ type: ACTIONS.COMMENT_UPDATE_STARTED });
|
||||
|
||||
return Comments.comment_edit({
|
||||
comment_id: comment_id,
|
||||
|
@ -786,17 +783,10 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
|||
})
|
||||
.then((result: CommentEditResponse) => {
|
||||
if (result != null) {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_UPDATE_COMPLETED,
|
||||
data: {
|
||||
comment: result,
|
||||
},
|
||||
});
|
||||
dispatch({ type: ACTIONS.COMMENT_UPDATE_COMPLETED, data: { comment: result } });
|
||||
} else {
|
||||
// the result will return null
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_UPDATE_FAILED,
|
||||
});
|
||||
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED });
|
||||
dispatch(
|
||||
doToast({
|
||||
message: 'Your channel is still being setup, try again in a few moments.',
|
||||
|
@ -806,10 +796,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_UPDATE_FAILED,
|
||||
data: error,
|
||||
});
|
||||
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED, data: error });
|
||||
dispatch(
|
||||
doToast({
|
||||
message: 'Unable to edit this comment, please try again later.',
|
||||
|
@ -819,7 +806,6 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
|||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function channelSignName(channelClaimId: string, channelName: string) {
|
||||
let signedObject;
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
@import 'component/channel-mention';
|
||||
@import 'component/claim-list';
|
||||
@import 'component/collection';
|
||||
@import 'component/comments';
|
||||
@import 'component/content';
|
||||
@import 'component/dat-gui';
|
||||
@import 'component/embed-player';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import '../../scss/component/form-field';
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
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;
|
||||
$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 {
|
||||
list-style-type: none;
|
||||
margin-left: var(--spacing-s);
|
||||
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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -44,7 +24,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.comment__author-thumbnail {
|
||||
.commentAuthor__thumbnail {
|
||||
@include handleChannelGif($thumbnailWidthSmall);
|
||||
margin-right: 0;
|
||||
|
||||
|
@ -67,7 +47,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__thumbnail-wrapper {
|
||||
.commentThumbnail__wrapper {
|
||||
flex: 0;
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
|
@ -77,7 +57,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
flex-direction: row;
|
||||
}
|
||||
|
||||
.comment__replies-container {
|
||||
.commentReplies__container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -99,17 +79,17 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment--top-level {
|
||||
.comment--topLevel {
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
|
||||
.comment--slimed {
|
||||
.comment__content--slimed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.comment__edit-input {
|
||||
.commentBody__editInput {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
|
@ -132,13 +112,13 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment--highlighted {
|
||||
.comment__content--highlighted {
|
||||
background: var(--color-comment-highlighted);
|
||||
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.comment__body-container {
|
||||
.commentBody__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
@ -164,7 +144,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.comment__meta-information {
|
||||
.commentMeta__information {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
@ -188,14 +168,14 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__badge--global-mod {
|
||||
.commentBadge__globalMod {
|
||||
.st0 {
|
||||
// @see: ICONS.BADGE_MOD
|
||||
fill: #fe7500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__badge--mod {
|
||||
.commentBadge__mod {
|
||||
.st0 {
|
||||
// @see: ICONS.BADGE_MOD
|
||||
fill: #ff3850;
|
||||
|
@ -422,64 +402,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticker__comment {
|
||||
.comment__message--sticker {
|
||||
margin-left: var(--spacing-m);
|
||||
height: 6rem;
|
||||
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 {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
box-shadow: 0 0 0 rgba(246, 72, 83, 0.4);
|
||||
|
@ -243,63 +87,7 @@ $recent-msg-button__height: 2rem;
|
|||
}
|
||||
}
|
||||
|
||||
.livestream-superchats__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(--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 {
|
||||
.livestreamSuperchat {
|
||||
display: flex;
|
||||
margin-right: var(--spacing-xs);
|
||||
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 {
|
||||
td:nth-of-type(1) {
|
||||
max-width: 4rem;
|
||||
|
|
|
@ -223,7 +223,7 @@
|
|||
}
|
||||
|
||||
@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-right: var(--spacing-m);
|
||||
}
|
||||
|
@ -234,7 +234,7 @@
|
|||
}
|
||||
|
||||
.main__right-side {
|
||||
width: var(--livestream-comments-width);
|
||||
width: var(--livestreamComments-width);
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
width: 100%;
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.super-chat--light {
|
||||
.superChat--light {
|
||||
position: absolute;
|
||||
display: inline;
|
||||
bottom: 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.super-chat {
|
||||
.superChat {
|
||||
border-radius: var(--border-radius);
|
||||
background: linear-gradient(to right, var(--color-superchat), var(--color-superchat-3));
|
||||
padding: 0.2rem var(--spacing-xs);
|
||||
|
@ -10,8 +10,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.super-chat--light {
|
||||
@extend .super-chat;
|
||||
.superChat--light {
|
||||
@extend .superChat;
|
||||
background: linear-gradient(to right, var(--color-superchat__light), var(--color-superchat-3__light));
|
||||
|
||||
.credit-amount {
|
||||
|
|
|
@ -247,7 +247,7 @@ textarea {
|
|||
margin: -1rem 0;
|
||||
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;
|
||||
vertical-align: middle;
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ $breakpoint-large: 1600px;
|
|||
|
||||
--tag-height: 1.5rem;
|
||||
|
||||
--livestream-comments-width: 30rem;
|
||||
--livestreamComments-width: 30rem;
|
||||
}
|
||||
|
||||
@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/claim-list';
|
||||
@import '../../ui/scss/component/collection';
|
||||
@import '../../ui/scss/component/comments';
|
||||
@import '../../ui/scss/component/content';
|
||||
@import '../../ui/scss/component/dat-gui';
|
||||
@import '../../ui/scss/component/embed-player';
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
@import '../../ui/scss/component/channel-mention';
|
||||
@import '../../ui/scss/component/claim-list';
|
||||
@import '../../ui/scss/component/collection';
|
||||
@import '../../ui/scss/component/comments';
|
||||
@import '../../ui/scss/component/content';
|
||||
@import '../../ui/scss/component/dat-gui';
|
||||
@import '../../ui/scss/component/embed-player';
|
||||
|
|
|
@ -98,7 +98,7 @@ $breakpoint-large: 1600px;
|
|||
|
||||
--tag-height: 1.5rem;
|
||||
|
||||
--livestream-comments-width: 30rem;
|
||||
--livestreamComments-width: 30rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
|
|
Loading…
Reference in a new issue