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:
saltrafael 2021-11-03 09:58:27 -03:00
parent 2974b35d21
commit d80be47fad
No known key found for this signature in database
GPG key ID: 85B63D36CBFAB1E5
39 changed files with 1063 additions and 1320 deletions

View file

@ -5,46 +5,47 @@ import {
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
selectMyChannelClaims, selectMyChannelClaims,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { doToast } from 'redux/actions/notifications';
import { doSetPlayingUri } from 'redux/actions/content';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { import {
selectLinkedCommentAncestors, selectLinkedCommentAncestors,
selectOthersReactsForComment, selectOthersReactsForComment,
makeSelectTotalReplyPagesForParentId, makeSelectTotalReplyPagesForParentId,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
import { doSetPlayingUri } from 'redux/actions/content';
import { doToast } from 'redux/actions/notifications';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import { selectPlayingUri } from 'redux/selectors/content'; import { selectPlayingUri } from 'redux/selectors/content';
import Comment from './view'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import CommentView from './view';
const select = (state, props) => { const select = (state, props) => {
const { channel_url: authorUri, comment_id: commentId } = props.comment;
const activeChannelClaim = selectActiveChannelClaim(state); const activeChannelClaim = selectActiveChannelClaim(state);
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId; const reactionKey = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
return { return {
channelIsBlocked: authorUri && makeSelectChannelIsMuted(authorUri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
othersReacts: selectOthersReactsForComment(state, reactionKey),
activeChannelClaim,
myChannels: selectMyChannelClaims(state),
playingUri: selectPlayingUri(state),
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
linkedCommentAncestors: selectLinkedCommentAncestors(state), linkedCommentAncestors: selectLinkedCommentAncestors(state),
totalReplyPages: makeSelectTotalReplyPagesForParentId(props.commentId)(state), myChannels: selectMyChannelClaims(state),
othersReacts: selectOthersReactsForComment(state, reactionKey),
playingUri: selectPlayingUri(state),
stakedLevel: makeSelectStakedLevelForChannelUri(authorUri)(state),
thumbnail: authorUri && makeSelectThumbnailForUri(authorUri)(state),
totalReplyPages: makeSelectTotalReplyPagesForParentId(commentId)(state),
userCanComment: selectUserVerifiedEmail(state),
}; };
}; };
const perform = (dispatch) => ({ const perform = (dispatch, ownProps) => ({
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })), clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)), updateComment: (editedComment) => dispatch(doCommentUpdate(ownProps.comment.comment_id, editedComment)),
fetchReplies: (uri, parentId, page, pageSize, sortBy) => fetchReplies: (page, pageSize, sortBy) =>
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)), dispatch(doCommentList(ownProps.uri, ownProps.comment.comment_id, page, pageSize, sortBy)),
doToast: (options) => dispatch(doToast(options)), doToast: (options) => dispatch(doToast(options)),
}); });
export default connect(select, perform)(Comment); export default connect(select, perform)(CommentView);

View file

@ -1,120 +1,100 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import 'scss/component/_comments.scss';
import * as PAGES from 'constants/pages'; import { ENABLE_COMMENT_REACTIONS } from 'config';
import * as KEYCODES from 'constants/keycodes';
import { COMMENT_HIGHLIGHTED } from 'constants/classnames';
import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
import { SITE_NAME, SIMPLE_SITE, ENABLE_COMMENT_REACTIONS } from 'config';
import React, { useEffect, useState } from 'react';
import { parseURI } from 'util/lbryURI';
import DateTime from 'component/dateTime';
import Button from 'component/button';
import Expandable from 'component/expandable';
import MarkdownPreview from 'component/common/markdown-preview';
import Tooltip from 'component/common/tooltip';
import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuButton } from '@reach/menu-button'; import { Menu, MenuButton } from '@reach/menu-button';
import Icon from 'component/common/icon'; import { parseSticker } from 'util/comments';
import { FormField, Form } from 'component/common/form'; import { parseURI } from 'util/lbryURI';
import classnames from 'classnames'; import { SORT_BY, COMMENT_PAGE_SIZE_REPLIES } from 'constants/comment';
import usePersistedState from 'effects/use-persisted-state';
import CommentReactions from 'component/commentReactions';
import CommentsReplies from 'component/commentsReplies';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames';
import CommentCreate from 'component/commentCreate'; import CommentCreate from 'component/commentCreate';
import CommentMenuList from 'component/commentMenuList'; import CommentMenuList from 'component/commentMenuList';
import UriIndicator from 'component/uriIndicator'; import CommentReactions from 'component/commentReactions';
import CommentsReplies from 'component/commentsReplies';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import DateTime from 'component/dateTime';
import Expandable from 'component/expandable';
import Icon from 'component/common/icon';
import MarkdownPreview from 'component/common/markdown-preview';
import OptimizedImage from 'component/optimizedImage'; import OptimizedImage from 'component/optimizedImage';
import { parseSticker } from 'util/comments'; import React, { useEffect, useState } from 'react';
import Tooltip from 'component/common/tooltip';
import UriIndicator from 'component/uriIndicator';
const AUTO_EXPAND_ALL_REPLIES = false; const AUTO_EXPAND_ALL_REPLIES = false;
type Props = {
clearPlayingUri: () => void,
uri: string,
claim: StreamClaim,
author: ?string, // LBRY Channel Name, e.g. @channel
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
commentId: string, // sha256 digest identifying the comment
message: string, // comment body
timePosted: number, // Comment timestamp
channelIsBlocked: boolean, // if the channel is blacklisted in the app
claimIsMine: boolean, // if you control the claim which this comment was posted on
commentIsMine: boolean, // if this comment was signed by an owned channel
updateComment: (string, string) => void,
fetchReplies: (string, string, number, number, number) => void,
totalReplyPages: number,
commentModBlock: (string) => void,
linkedCommentId?: string,
linkedCommentAncestors: { [string]: Array<string> },
myChannels: ?Array<ChannelClaim>,
commentingEnabled: boolean,
doToast: ({ message: string }) => void,
isTopLevel?: boolean,
threadDepth: number,
hideActions?: boolean,
isPinned: boolean,
othersReacts: ?{
like: number,
dislike: number,
},
commentIdentityChannel: any,
activeChannelClaim: ?ChannelClaim,
playingUri: ?PlayingUri,
stakedLevel: number,
supportAmount: number,
numDirectReplies: number,
isModerator: boolean,
isGlobalMod: boolean,
isFiat: boolean,
supportDisabled: boolean,
setQuickReply: (any) => void,
quickReply: any,
};
const LENGTH_TO_COLLAPSE = 300; const LENGTH_TO_COLLAPSE = 300;
function Comment(props: Props) { type Props = {
channelIsBlocked: boolean, // if the channel is blacklisted in the app
claim: StreamClaim,
claimIsMine: boolean, // if you control the claim which this comment was posted on
comment: Comment,
commentIdentityChannel: any,
hideActions?: boolean,
isTopLevel?: boolean,
linkedCommentAncestors: { [string]: Array<string> },
linkedCommentId?: string,
myChannels: ?Array<ChannelClaim>,
othersReacts: ?{ like: number, dislike: number },
playingUri: ?PlayingUri,
quickReply: any,
stakedLevel: number,
supportDisabled: boolean,
threadDepth: number,
totalReplyPages: number,
uri: string,
userCanComment: boolean,
clearPlayingUri: () => void,
commentModBlock: (string) => void,
fetchReplies: (number, number, string) => void,
setQuickReply: (any) => void,
updateComment: (string) => void,
};
function CommentView(props: Props) {
const { const {
clearPlayingUri,
claim,
uri,
author,
authorUri,
timePosted,
message,
channelIsBlocked, channelIsBlocked,
commentIsMine, claim,
commentId, comment,
updateComment,
fetchReplies,
totalReplyPages,
linkedCommentId,
linkedCommentAncestors,
commentingEnabled,
myChannels,
doToast,
isTopLevel,
threadDepth,
hideActions, hideActions,
isPinned, isTopLevel,
linkedCommentAncestors,
linkedCommentId,
myChannels,
othersReacts, othersReacts,
playingUri, playingUri,
stakedLevel,
supportAmount,
numDirectReplies,
isModerator,
isGlobalMod,
isFiat,
supportDisabled,
setQuickReply,
quickReply, quickReply,
stakedLevel,
supportDisabled,
threadDepth,
totalReplyPages,
uri,
userCanComment,
clearPlayingUri,
fetchReplies,
setQuickReply,
updateComment,
} = props; } = props;
const { const {
push, channel_id: authorId,
channel_url: authorUri,
comment_id: commentId,
comment: message,
is_fiat: isFiat,
is_global_mod: isGlobalMod,
is_moderator: isModerator,
is_pinned: isPinned,
replies: numDirectReplies,
support_amount: supportAmount,
} = comment;
const timePosted = comment.timestamp * 1000;
const commentIsMine = authorId && myChannels && myChannels.some(({ claim_id }) => claim_id === authorId);
const {
replace, replace,
location: { pathname, search }, location: { pathname, search },
} = useHistory(); } = useHistory();
@ -129,79 +109,22 @@ function Comment(props: Props) {
const [isReplying, setReplying] = React.useState(false); const [isReplying, setReplying] = React.useState(false);
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
const [editedMessage, setCommentValue] = useState(message); const [editedMessage, setCommentValue] = useState(message);
const [charCount, setCharCount] = useState(editedMessage.length);
const [showReplies, setShowReplies] = useState(showRepliesOnMount); const [showReplies, setShowReplies] = useState(showRepliesOnMount);
const [page, setPage] = useState(showRepliesOnMount ? 1 : 0); const [page, setPage] = useState(showRepliesOnMount ? 1 : 0);
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
const [displayDeadComment, setDisplayDeadComment] = React.useState(false); const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
const hasChannels = myChannels && myChannels.length > 0;
const likesCount = (othersReacts && othersReacts.like) || 0; const likesCount = (othersReacts && othersReacts.like) || 0;
const dislikesCount = (othersReacts && othersReacts.dislike) || 0; const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
const totalLikesAndDislikes = likesCount + dislikesCount; const totalLikesAndDislikes = likesCount + dislikesCount;
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8; const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri; const commentByContentOwner = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
const stickerFromMessage = parseSticker(message); const stickerFromMessage = parseSticker(message);
let channelOwnerOfContent; let contentOwnerChannel;
try { try {
const { channelName } = parseURI(uri); ({ channelName: contentOwnerChannel } = parseURI(uri));
if (channelName) {
channelOwnerOfContent = channelName;
}
} catch (e) {} } catch (e) {}
useEffect(() => {
if (isEditing) {
setCharCount(editedMessage.length);
// a user will try and press the escape key to cancel editing their comment
const handleEscape = (event) => {
if (event.keyCode === KEYCODES.ESCAPE) {
setEditing(false);
}
};
window.addEventListener('keydown', handleEscape);
// removes the listener so it doesn't cause problems elsewhere in the app
return () => {
window.removeEventListener('keydown', handleEscape);
};
}
}, [author, authorUri, editedMessage, isEditing, setEditing]);
useEffect(() => {
if (page > 0) {
fetchReplies(uri, commentId, page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST);
}
}, [page, uri, commentId, fetchReplies]);
function handleEditMessageChanged(event) {
setCommentValue(!SIMPLE_SITE && advancedEditor ? event : event.target.value);
}
function handleEditComment() {
if (playingUri && playingUri.source === 'comment') {
clearPlayingUri();
}
setEditing(true);
}
function handleSubmit() {
updateComment(commentId, editedMessage);
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
setEditing(false);
}
function handleCommentReply() {
if (!hasChannels) {
push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`);
doToast({ message: __('A channel is required to comment on %SITE_NAME%', { SITE_NAME }) });
} else {
setReplying(!isReplying);
}
}
function handleTimeClick() { function handleTimeClick() {
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
urlParams.delete('lc'); urlParams.delete('lc');
@ -221,10 +144,27 @@ function Comment(props: Props) {
} }
}, []); }, []);
useEffect(() => {
if (page > 0) fetchReplies(page, COMMENT_PAGE_SIZE_REPLIES, SORT_BY.OLDEST);
}, [commentId, fetchReplies, page, uri]);
const commentBadge = (label: string, className: string, icon: string) => (
<Tooltip label={label}>
<span className={`comment__badge ${className}`}>
<Icon icon={icon} size={20} />
</span>
</Tooltip>
);
const MarkdownWrapper =
editedMessage.length >= LENGTH_TO_COLLAPSE
? ({ children }) => <Expandable>{children}</Expandable>
: ({ children }) => children;
return ( return (
<li <li
className={classnames('comment', { className={classnames('comment', {
'comment--top-level': isTopLevel, 'comment--topLevel': isTopLevel,
'comment--reply': !isTopLevel, 'comment--reply': !isTopLevel,
'comment--superchat': supportAmount > 0, 'comment--superchat': supportAmount > 0,
})} })}
@ -233,48 +173,25 @@ function Comment(props: Props) {
<div <div
ref={isLinkedComment ? linkedCommentRef : undefined} ref={isLinkedComment ? linkedCommentRef : undefined}
className={classnames('comment__content', { className={classnames('comment__content', {
[COMMENT_HIGHLIGHTED]: isLinkedComment, 'comment__content--highlighted': isLinkedComment,
'comment--slimed': slimedToDeath && !displayDeadComment, 'comment__content--slimed': slimedToDeath && !displayDeadComment,
})} })}
> >
<div className="comment__thumbnail-wrapper"> <div className="commentThumbnail__wrapper">
{authorUri ? ( <ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="commentAuthor__thumbnail" />
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" />
) : (
<ChannelThumbnail xsmall className="comment__author-thumbnail" />
)}
</div> </div>
<div className="comment__body-container"> <div className="commentBody__container">
<div className="comment__meta"> <div className="comment__meta">
<div className="comment__meta-information"> <div className="commentMeta__information">
{isGlobalMod && ( {isModerator && commentBadge(__('Moderator'), 'commentBadge__mod', ICONS.BADGE_MOD)}
<Tooltip label={__('Admin')}> {isGlobalMod && commentBadge(__('Admin'), 'commentBadge__globalMod', ICONS.BADGE_MOD)}
<span className="comment__badge comment__badge--global-mod">
<Icon icon={ICONS.BADGE_MOD} size={20} />
</span>
</Tooltip>
)}
{isModerator && ( <UriIndicator
<Tooltip label={__('Moderator')}> uri={authorUri}
<span className="comment__badge comment__badge--mod"> className={classnames('comment__author', { 'comment__author--creator': commentByContentOwner })}
<Icon icon={ICONS.BADGE_MOD} size={20} /> link
</span> />
</Tooltip>
)}
{!author ? (
<span className="comment__author">{__('Anonymous')}</span>
) : (
<UriIndicator
className={classnames('comment__author', {
'comment__author--creator': commentByOwnerOfContent,
})}
link
uri={authorUri}
/>
)}
<Button <Button
className="comment__time" className="comment__time"
onClick={handleTimeClick} onClick={handleTimeClick}
@ -286,8 +203,8 @@ function Comment(props: Props) {
{isPinned && ( {isPinned && (
<span className="comment__pin"> <span className="comment__pin">
<Icon icon={ICONS.PIN} size={14} /> <Icon icon={ICONS.PIN} size={14} />
{channelOwnerOfContent {contentOwnerChannel
? __('Pinned by @%channel%', { channel: channelOwnerOfContent }) ? __('Pinned by @%channel%', { channel: contentOwnerChannel })
: __('Pinned by creator')} : __('Pinned by creator')}
</span> </span>
)} )}
@ -304,63 +221,57 @@ function Comment(props: Props) {
commentId={commentId} commentId={commentId}
authorUri={authorUri} authorUri={authorUri}
commentIsMine={commentIsMine} commentIsMine={commentIsMine}
handleEditComment={handleEditComment} handleEditComment={() => {
if (playingUri && playingUri.source === 'comment') clearPlayingUri();
setEditing(true);
}}
supportAmount={supportAmount} supportAmount={supportAmount}
setQuickReply={setQuickReply} setQuickReply={setQuickReply}
disableEdit={Boolean(stickerFromMessage)}
/> />
</Menu> </Menu>
</div> </div>
</div> </div>
<div> <div className="comment__body">
{isEditing ? ( {isEditing ? (
<Form onSubmit={handleSubmit}> <CommentCreate
<FormField isEdit
className="comment__edit-input" editedMessage={message}
type={!SIMPLE_SITE && advancedEditor ? 'markdown' : 'textarea'} onDoneEditing={(editedMessage) => {
name="editing_comment" if (editedMessage) {
value={editedMessage} updateComment(editedMessage);
charCount={charCount} if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
onChange={handleEditMessageChanged} setCommentValue(editedMessage);
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT} }
/> setEditing(false);
<div className="section__actions section__actions--no-margin"> }}
<Button supportDisabled
button="primary" />
type="submit"
label={__('Done')}
requiresAuth={IS_WEB}
disabled={message === editedMessage}
/>
<Button button="link" label={__('Cancel')} onClick={() => setEditing(false)} />
</div>
</Form>
) : ( ) : (
<> <>
<div className="comment__message"> <div
className={classnames('comment__message', {
'comment__message--dead': slimedToDeath && !displayDeadComment,
'comment__message--sticker': stickerFromMessage,
})}
onClick={() => slimedToDeath && setDisplayDeadComment(true)}
>
{slimedToDeath && !displayDeadComment ? ( {slimedToDeath && !displayDeadComment ? (
<div onClick={() => setDisplayDeadComment(true)} className="comment__dead"> <>
{__('This comment was slimed to death.')} <Icon icon={ICONS.SLIME_ACTIVE} /> {__('This comment was slimed to death.')}
</div> <Icon icon={ICONS.SLIME_ACTIVE} />
</>
) : stickerFromMessage ? ( ) : stickerFromMessage ? (
<div className="sticker__comment"> <OptimizedImage src={stickerFromMessage.url} waitLoad />
<OptimizedImage src={stickerFromMessage.url} waitLoad /> ) : (
</div> <MarkdownWrapper>
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
<Expandable>
<MarkdownPreview <MarkdownPreview
content={message} content={message}
promptLinks
parentCommentId={commentId} parentCommentId={commentId}
stakedLevel={stakedLevel} stakedLevel={stakedLevel}
promptLinks
/> />
</Expandable> </MarkdownWrapper>
) : (
<MarkdownPreview
content={message}
promptLinks
parentCommentId={commentId}
stakedLevel={stakedLevel}
/>
)} )}
</div> </div>
@ -368,10 +279,10 @@ function Comment(props: Props) {
<div className="comment__actions"> <div className="comment__actions">
{threadDepth !== 0 && ( {threadDepth !== 0 && (
<Button <Button
requiresAuth={IS_WEB} requiresAuth
label={commentingEnabled ? __('Reply') : __('Log in to reply')} label={userCanComment ? __('Reply') : __('Log in to reply')}
className="comment__action" className="comment__action"
onClick={handleCommentReply} onClick={() => setReplying(!isReplying)}
icon={ICONS.REPLY} icon={ICONS.REPLY}
/> />
)} )}
@ -379,36 +290,33 @@ function Comment(props: Props) {
</div> </div>
)} )}
{numDirectReplies > 0 && !showReplies && ( {numDirectReplies > 0 &&
<div className="comment__actions"> (!showReplies ? (
<Button <div className="comment__actions">
label={ <Button
numDirectReplies < 2 label={
? __('Show reply') numDirectReplies < 2
: __('Show %count% replies', { count: numDirectReplies }) ? __('Show reply')
} : __('Show %count% replies', { count: numDirectReplies })
button="link"
onClick={() => {
setShowReplies(true);
if (page === 0) {
setPage(1);
} }
}} button="link"
icon={ICONS.DOWN} onClick={() => {
/> setShowReplies(true);
</div> if (page === 0) setPage(1);
)} }}
icon={ICONS.DOWN}
{numDirectReplies > 0 && showReplies && ( />
<div className="comment__actions"> </div>
<Button ) : (
label={__('Hide replies')} <div className="comment__actions">
button="link" <Button
onClick={() => setShowReplies(false)} label={__('Hide replies')}
icon={ICONS.UP} button="link"
/> onClick={() => setShowReplies(false)}
</div> icon={ICONS.UP}
)} />
</div>
))}
{isReplying && ( {isReplying && (
<CommentCreate <CommentCreate
@ -419,9 +327,7 @@ function Comment(props: Props) {
setShowReplies(true); setShowReplies(true);
setReplying(false); setReplying(false);
}} }}
onCancelReplying={() => { onCancelReplying={() => setReplying(false)}
setReplying(false);
}}
supportDisabled={supportDisabled} supportDisabled={supportDisabled}
/> />
)} )}
@ -446,4 +352,4 @@ function Comment(props: Props) {
); );
} }
export default Comment; export default CommentView;

View file

@ -21,7 +21,7 @@ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
isFetchingChannels: selectFetchingMyChannels(state), isFetchingChannels: selectFetchingMyChannels(state),
settingsByChannelId: selectSettingsByChannelId(state), settingsByChannelId: selectSettingsByChannelId(state),
supportDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state), supportDisabled: props.supportDisabled || makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_SUPPORT_TAG)(state),
}); });
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({

View file

@ -45,7 +45,9 @@ type Props = {
channels: ?Array<ChannelClaim>, channels: ?Array<ChannelClaim>,
claim: StreamClaim, claim: StreamClaim,
claimIsMine: boolean, claimIsMine: boolean,
editedMessage?: string,
embed?: boolean, embed?: boolean,
isEdit?: boolean,
isFetchingChannels: boolean, isFetchingChannels: boolean,
isNested: boolean, isNested: boolean,
isReply: boolean, isReply: boolean,
@ -60,6 +62,7 @@ type Props = {
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
fetchComment: (commentId: string) => Promise<any>, fetchComment: (commentId: string) => Promise<any>,
onCancelReplying?: () => void, onCancelReplying?: () => void,
onDoneEditing?: (editedMessage?: string) => void,
onDoneReplying?: () => void, onDoneReplying?: () => void,
sendCashTip: (TipParams, UserParams, string, ?string, (any) => void) => string, sendCashTip: (TipParams, UserParams, string, ?string, (any) => void) => string,
sendTip: ({}, (any) => void, (any) => void) => void, sendTip: ({}, (any) => void, (any) => void) => void,
@ -74,7 +77,9 @@ export function CommentCreate(props: Props) {
channels, channels,
claim, claim,
claimIsMine, claimIsMine,
editedMessage,
embed, embed,
isEdit,
isFetchingChannels, isFetchingChannels,
isNested, isNested,
isReply, isReply,
@ -91,6 +96,7 @@ export function CommentCreate(props: Props) {
onCancelReplying, onCancelReplying,
onDoneReplying, onDoneReplying,
sendCashTip, sendCashTip,
onDoneEditing,
sendTip, sendTip,
setQuickReply, setQuickReply,
} = props; } = props;
@ -114,7 +120,7 @@ export function CommentCreate(props: Props) {
const [selectedSticker, setSelectedSticker] = React.useState(); const [selectedSticker, setSelectedSticker] = React.useState();
const [tipAmount, setTipAmount] = React.useState(1); const [tipAmount, setTipAmount] = React.useState(1);
const [convertedAmount, setConvertedAmount] = React.useState(); const [convertedAmount, setConvertedAmount] = React.useState();
const [commentValue, setCommentValue] = React.useState(''); const [commentValue, setCommentValue] = React.useState(editedMessage || '');
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false); const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
const [stickerSelector, setStickerSelector] = React.useState(); const [stickerSelector, setStickerSelector] = React.useState();
const [activeTab, setActiveTab] = React.useState(''); const [activeTab, setActiveTab] = React.useState('');
@ -474,16 +480,18 @@ export function CommentCreate(props: Props) {
<FormField <FormField
disabled={isFetchingChannels} disabled={isFetchingChannels}
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'} type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
name={isReply ? 'content_reply' : 'content_description'} name={(isReply && 'content_reply') || (isEdit && 'editing_comment') || 'content_description'}
ref={formFieldRef} ref={formFieldRef}
className={isReply ? 'content_reply' : 'content_comment'} className={(isReply && 'content_reply') || (isEdit && 'commentEdit__input') || 'content_comment'}
label={ label={
<span className="commentCreate__labelWrapper"> !isEdit && (
{!livestream && ( <span className="commentCreate__labelWrapper">
<div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div> {!livestream && (
)} <div className="commentCreate__label">{isReply ? __('Replying as ') : __('Comment as ')}</div>
<SelectChannel tiny /> )}
</span> <SelectChannel tiny />
</span>
)
} }
quickActionLabel={ quickActionLabel={
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')) !SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
@ -577,16 +585,17 @@ export function CommentCreate(props: Props) {
disabled={disabled || stickerSelector} disabled={disabled || stickerSelector}
type="submit" type="submit"
label={ label={
isReply (isEdit && __('Done')) ||
? isSubmitting (isSubmitting
? __('Replying...') ? (isReply && __('Replying...')) || __('Commenting...')
: __('Reply') : (isReply && __('Reply')) || __('Comment --[button to submit something]--'))
: isSubmitting
? __('Commenting...')
: __('Comment --[button to submit something]--')
} }
requiresAuth requiresAuth
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()} onClick={() =>
activeChannelClaim &&
commentValue.length &&
(isEdit && editedMessage && onDoneEditing ? onDoneEditing() : handleCreateComment())
}
/> />
) )
)} )}
@ -629,7 +638,8 @@ export function CommentCreate(props: Props) {
isReviewingSupportComment || isReviewingSupportComment ||
stickerSelector || stickerSelector ||
isReviewingStickerComment || isReviewingStickerComment ||
(isReply && !minTip)) && ( (isReply && !minTip) ||
isEdit) && (
<Button <Button
disabled={isSupportComment && isSubmitting} disabled={isSupportComment && isSubmitting}
button="link" button="link"
@ -648,6 +658,8 @@ export function CommentCreate(props: Props) {
setStickerSelector(false); setStickerSelector(false);
} else if (isReply && !minTip && onCancelReplying) { } else if (isReply && !minTip && onCancelReplying) {
onCancelReplying(); onCancelReplying();
} else if (isEdit && onDoneEditing) {
onDoneEditing();
} }
}} }}
/> />

View file

@ -1,11 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims';
import {
makeSelectClaimForUri,
makeSelectClaimIsMine,
selectFetchingMyChannels,
selectMyChannelClaims,
} from 'redux/selectors/claims';
import { import {
selectTopLevelCommentsForUri, selectTopLevelCommentsForUri,
makeSelectTopLevelTotalPagesForUri, makeSelectTopLevelTotalPagesForUri,
@ -20,6 +13,8 @@ import {
selectPinnedCommentsForUri, selectPinnedCommentsForUri,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments'; import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
import { doResolveUris } from 'redux/actions/claims';
import { makeSelectClaimForUri, makeSelectClaimIsMine, selectFetchingMyChannels } from 'redux/selectors/claims';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import CommentsList from './view'; import CommentsList from './view';
@ -33,32 +28,31 @@ const select = (state, props) => {
: []; : [];
return { return {
topLevelComments, activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
resolvedComments,
myChannels: selectMyChannelClaims(state),
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state), allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
pinnedComments: selectPinnedCommentsForUri(state, props.uri),
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
fetchingChannels: selectFetchingMyChannels(state),
isFetchingComments: selectIsFetchingComments(state), isFetchingComments: selectIsFetchingComments(state),
isFetchingCommentsById: selectIsFetchingCommentsById(state), isFetchingCommentsById: selectIsFetchingCommentsById(state),
isFetchingReacts: selectIsFetchingReacts(state), isFetchingReacts: selectIsFetchingReacts(state),
fetchingChannels: selectFetchingMyChannels(state),
settingsByChannelId: selectSettingsByChannelId(state),
myReactsByCommentId: selectMyReacts(state), myReactsByCommentId: selectMyReacts(state),
othersReactsById: selectOthersReacts(state), othersReactsById: selectOthersReacts(state),
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id, pinnedComments: selectPinnedCommentsForUri(state, props.uri),
resolvedComments,
settingsByChannelId: selectSettingsByChannelId(state),
topLevelComments,
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
}; };
}; };
const perform = (dispatch) => ({ const perform = (dispatch, ownProps) => ({
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)), doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
fetchComment: (commentId) => dispatch(doCommentById(commentId)), fetchComment: (commentId) => dispatch(doCommentById(commentId)),
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)), fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
resetComments: (claimId) => dispatch(doCommentReset(claimId)), fetchTopLevelComments: (page, pageSize, sortBy) => dispatch(doCommentList(ownProps.uri, '', page, pageSize, sortBy)),
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)), resetComments: () => ownProps.claim && dispatch(doCommentReset(ownProps.claim.claimId)),
}); });
export default connect(select, perform)(CommentsList); export default connect(select, perform)(CommentsList);

View file

@ -1,7 +1,9 @@
// @flow // @flow
import 'scss/component/_comments-list.scss';
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment'; import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
import { ENABLE_COMMENT_REACTIONS } from 'config'; import { ENABLE_COMMENT_REACTIONS } from 'config';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { scaleToDevicePixelRatio } from 'util/scale';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as REACTION_TYPES from 'constants/reactions'; import * as REACTION_TYPES from 'constants/reactions';
@ -18,67 +20,57 @@ import usePersistedState from 'effects/use-persisted-state';
const DEBOUNCE_SCROLL_HANDLER_MS = 200; const DEBOUNCE_SCROLL_HANDLER_MS = 200;
function scaleToDevicePixelRatio(value) {
const devicePixelRatio = window.devicePixelRatio || 1.0;
if (devicePixelRatio < 1.0) {
return Math.ceil(value / devicePixelRatio);
}
return Math.ceil(value * devicePixelRatio);
}
type Props = { type Props = {
activeChannelId: ?string,
allCommentIds: any, allCommentIds: any,
pinnedComments: Array<Comment>,
topLevelComments: Array<Comment>,
resolvedComments: Array<Comment>,
topLevelTotalPages: number,
uri: string,
claim: ?Claim, claim: ?Claim,
claimIsMine: boolean, claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>, commentsAreExpanded?: boolean,
fetchingChannels: boolean,
isFetchingComments: boolean, isFetchingComments: boolean,
isFetchingCommentsById: boolean, isFetchingCommentsById: boolean,
isFetchingReacts: boolean, isFetchingReacts: boolean,
linkedCommentId?: string, linkedCommentId?: string,
totalComments: number,
fetchingChannels: boolean,
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation) myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } }, othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
activeChannelId: ?string, pinnedComments: Array<Comment>,
resolvedComments: Array<Comment>,
settingsByChannelId: { [channelId: string]: PerChannelSettings }, settingsByChannelId: { [channelId: string]: PerChannelSettings },
commentsAreExpanded?: boolean, topLevelComments: Array<Comment>,
fetchReacts: (Array<string>) => Promise<any>, topLevelTotalPages: number,
totalComments: number,
uri: string,
doResolveUris: (Array<string>) => void, doResolveUris: (Array<string>) => void,
fetchTopLevelComments: (string, number, number, number) => void,
fetchComment: (string) => void, fetchComment: (string) => void,
resetComments: (string) => void, fetchReacts: (Array<string>) => Promise<any>,
fetchTopLevelComments: (number, number, string) => void,
resetComments: () => void,
}; };
function CommentList(props: Props) { function CommentsList(props: Props) {
const { const {
activeChannelId,
allCommentIds, allCommentIds,
uri,
pinnedComments,
topLevelComments,
resolvedComments,
topLevelTotalPages,
claim, claim,
claimIsMine, claimIsMine,
myChannels, commentsAreExpanded,
doResolveUris,
fetchingChannels,
fetchReacts,
isFetchingComments, isFetchingComments,
isFetchingReacts, isFetchingReacts,
linkedCommentId, linkedCommentId,
totalComments,
fetchingChannels,
myReactsByCommentId, myReactsByCommentId,
othersReactsById, othersReactsById,
activeChannelId, pinnedComments,
resolvedComments,
settingsByChannelId, settingsByChannelId,
commentsAreExpanded, topLevelComments,
fetchReacts, topLevelTotalPages,
doResolveUris, totalComments,
fetchTopLevelComments, uri,
fetchComment, fetchComment,
fetchTopLevelComments,
resetComments, resetComments,
} = props; } = props;
@ -92,6 +84,7 @@ function CommentList(props: Props) {
const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments); const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
const hasDefaultExpansion = commentsAreExpanded || desktopView; const hasDefaultExpansion = commentsAreExpanded || desktopView;
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion); const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
const channelId = getChannelIdFromClaim(claim); const channelId = getChannelIdFromClaim(claim);
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined; const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
@ -106,7 +99,7 @@ function CommentList(props: Props) {
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
); );
function changeSort(newSort) { function changeSort(newSort: string) {
if (sort !== newSort) { if (sort !== newSort) {
setSort(newSort); setSort(newSort);
setPage(0); // Invalidate existing comments setPage(0); // Invalidate existing comments
@ -116,9 +109,7 @@ function CommentList(props: Props) {
// Reset comments // Reset comments
useEffect(() => { useEffect(() => {
if (page === 0) { if (page === 0) {
if (claim) { if (claim) resetComments();
resetComments(claim.claim_id);
}
setPage(1); setPage(1);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -131,7 +122,7 @@ function CommentList(props: Props) {
fetchComment(linkedCommentId); fetchComment(linkedCommentId);
} }
fetchTopLevelComments(uri, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort); fetchTopLevelComments(page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
} }
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]); }, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]);
@ -237,39 +228,25 @@ function CommentList(props: Props) {
if (!topLevelComments || alreadyResolved) return; if (!topLevelComments || alreadyResolved) return;
const urisToResolve = []; const urisToResolve = [];
topLevelComments.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url)); topLevelComments.forEach(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
if (urisToResolve.length > 0) doResolveUris(urisToResolve); if (urisToResolve.length > 0) doResolveUris(urisToResolve);
}, [alreadyResolved, doResolveUris, topLevelComments]); }, [alreadyResolved, doResolveUris, topLevelComments]);
const getCommentElems = (comments) => const getCommentElems = (comments: Array<Comment>) =>
comments.map((comment) => ( comments.map((comment: Comment) => (
<CommentView <CommentView
isTopLevel
threadDepth={3}
key={comment.comment_id}
uri={uri}
authorUri={comment.channel_url}
author={comment.channel_name}
claimId={comment.claim_id}
commentId={comment.comment_id}
message={comment.comment}
timePosted={comment.timestamp * 1000}
claimIsMine={claimIsMine} claimIsMine={claimIsMine}
commentIsMine={ comment={comment}
comment.channel_id && myChannels && myChannels.some(({ claim_id }) => claim_id === comment.channel_id) isTopLevel
} key={comment.comment_id}
linkedCommentId={linkedCommentId} linkedCommentId={linkedCommentId}
isPinned={comment.is_pinned} threadDepth={3}
supportAmount={comment.support_amount} uri={uri}
numDirectReplies={comment.replies}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
/> />
)); ));
const sortButton = (label, icon, sortOption) => ( const sortButton = (label: string, icon: string, sortOption: string) => (
<Button <Button
button="alt" button="alt"
label={label} label={label}
@ -311,8 +288,7 @@ function CommentList(props: Props) {
)} )}
<ul <ul
className={classnames({ className={classnames('comments', {
comments: desktopView || expandedComments,
'comments--contracted': !desktopView && !expandedComments, 'comments--contracted': !desktopView && !expandedComments,
})} })}
> >
@ -352,4 +328,4 @@ function CommentList(props: Props) {
); );
} }
export default CommentList; export default CommentsList;

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doResolveUris } from 'redux/actions/claims'; import { doResolveUris } from 'redux/actions/claims';
import { makeSelectClaimIsMine, selectMyChannelClaims, makeSelectClaimForUri } from 'redux/selectors/claims'; import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'redux/selectors/claims';
import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments'; import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import CommentsReplies from './view'; import CommentsReplies from './view';
@ -13,12 +13,11 @@ const select = (state, props) => {
: []; : [];
return { return {
fetchedReplies,
resolvedReplies,
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
userCanComment: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, fetchedReplies,
myChannels: selectMyChannelClaims(state),
isFetchingByParentId: selectIsFetchingCommentsByParentId(state), isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
resolvedReplies,
userCanComment: selectUserVerifiedEmail(state),
}; };
}; };

View file

@ -1,4 +1,5 @@
// @flow // @flow
import 'scss/component/_comments.scss';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import Button from 'component/button'; import Button from 'component/button';
import Comment from 'component/comment'; import Comment from 'component/comment';
@ -6,44 +7,43 @@ import React from 'react';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
type Props = { type Props = {
fetchedReplies: Array<Comment>,
resolvedReplies: Array<Comment>,
uri: string,
parentId: string,
claimIsMine: boolean, claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>, fetchedReplies: Array<Comment>,
linkedCommentId?: string,
userCanComment: boolean,
threadDepth: number,
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
isFetchingByParentId: { [string]: boolean },
hasMore: boolean, hasMore: boolean,
isFetchingByParentId: { [string]: boolean },
linkedCommentId?: string,
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
parentId: string,
resolvedReplies: Array<Comment>,
supportDisabled: boolean, supportDisabled: boolean,
threadDepth: number,
uri: string,
userCanComment: boolean,
doResolveUris: (Array<string>) => void, doResolveUris: (Array<string>) => void,
onShowMore?: () => void, onShowMore?: () => void,
}; };
function CommentsReplies(props: Props) { function CommentsReplies(props: Props) {
const { const {
uri,
parentId,
fetchedReplies,
resolvedReplies,
claimIsMine, claimIsMine,
myChannels, fetchedReplies,
linkedCommentId,
userCanComment,
threadDepth,
numDirectReplies,
isFetchingByParentId,
hasMore, hasMore,
isFetchingByParentId,
linkedCommentId,
numDirectReplies,
parentId,
resolvedReplies,
supportDisabled, supportDisabled,
threadDepth,
uri,
userCanComment,
doResolveUris, doResolveUris,
onShowMore, onShowMore,
} = props; } = props;
const [isExpanded, setExpanded] = React.useState(true); const [isExpanded, setExpanded] = React.useState(true);
const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies); const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies);
const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length; const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length;
const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0; const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0;
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length; const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length;
@ -53,7 +53,7 @@ function CommentsReplies(props: Props) {
if (!fetchedReplies || alreadyResolved) return; if (!fetchedReplies || alreadyResolved) return;
const urisToResolve = []; const urisToResolve = [];
fetchedReplies.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url)); fetchedReplies.forEach(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
if (urisToResolve.length > 0) doResolveUris(urisToResolve); if (urisToResolve.length > 0) doResolveUris(urisToResolve);
}, [alreadyResolved, doResolveUris, fetchedReplies]); }, [alreadyResolved, doResolveUris, fetchedReplies]);
@ -65,7 +65,7 @@ function CommentsReplies(props: Props) {
}, [isResolvingReplies, fetchedReplies]); }, [isResolvingReplies, fetchedReplies]);
return !numDirectReplies ? null : ( return !numDirectReplies ? null : (
<div className="comment__replies-container"> <div className="commentReplies__container">
{!isExpanded ? ( {!isExpanded ? (
<div className="comment__actions--nested"> <div className="comment__actions--nested">
<Button <Button
@ -83,35 +83,22 @@ function CommentsReplies(props: Props) {
{!isResolvingReplies && {!isResolvingReplies &&
commentsToDisplay && commentsToDisplay &&
commentsToDisplay.length > 0 && commentsToDisplay.length > 0 &&
commentsToDisplay.map((comment) => ( commentsToDisplay.map((comment: Comment) => (
<Comment <Comment
claimIsMine={claimIsMine}
comment={comment}
commentingEnabled={userCanComment}
key={comment.comment_id}
linkedCommentId={linkedCommentId}
supportDisabled={supportDisabled}
threadDepth={threadDepth} threadDepth={threadDepth}
uri={uri} uri={uri}
authorUri={comment.channel_url}
author={comment.channel_name}
claimId={comment.claim_id}
commentId={comment.comment_id}
key={comment.comment_id}
message={comment.comment}
timePosted={comment.timestamp * 1000}
claimIsMine={claimIsMine}
commentIsMine={
comment.channel_id &&
myChannels &&
myChannels.some(({ claim_id }) => claim_id === comment.channel_id)
}
linkedCommentId={linkedCommentId}
commentingEnabled={userCanComment}
supportAmount={comment.support_amount}
numDirectReplies={comment.replies}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
supportDisabled={supportDisabled}
/> />
))} ))}
</ul> </ul>
</div> </div>
)} )}
{isExpanded && fetchedReplies && hasMore && ( {isExpanded && fetchedReplies && hasMore && (
<div className="comment__actions--nested"> <div className="comment__actions--nested">
<Button <Button
@ -122,11 +109,10 @@ function CommentsReplies(props: Props) {
/> />
</div> </div>
)} )}
{(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && ( {(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && (
<div className="comment__replies-container"> <div className="comment__actions--nested">
<div className="comment__actions--nested"> <Spinner type="small" />
<Spinner type="small" />
</div>
</div> </div>
)} )}
</div> </div>

View file

@ -92,8 +92,8 @@ class CreditAmount extends React.PureComponent<Props> {
<span <span
title={fullPrice} title={fullPrice}
className={classnames(className, { className={classnames(className, {
'super-chat': superChat, superChat: superChat,
'super-chat--light': superChatLight, 'superChat--light': superChatLight,
})} })}
> >
<span className="credit-amount">{amountText}</span> <span className="credit-amount">{amountText}</span>

View file

@ -1,17 +1,10 @@
// @flow // @flow
import React from 'react'; import { scaleToDevicePixelRatio } from 'util/scale';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import React from 'react';
const DEBOUNCE_SCROLL_HANDLER_MS = 50; const DEBOUNCE_SCROLL_HANDLER_MS = 50;
function scaleToDevicePixelRatio(value) {
const devicePixelRatio = window.devicePixelRatio || 1.0;
if (devicePixelRatio < 1.0) {
return Math.ceil(value / devicePixelRatio);
}
return Math.ceil(value * devicePixelRatio);
}
type Props = { type Props = {
children: any, children: any,
skipWait?: boolean, skipWait?: boolean,

View file

@ -1,10 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectStakedLevelForChannelUri, selectClaimForUri } from 'redux/selectors/claims'; import { makeSelectStakedLevelForChannelUri, selectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims';
import LivestreamComment from './view'; import LivestreamComment from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: selectClaimForUri(state, props.uri), claim: selectClaimForUri(state, props.uri),
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state), stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
myChannels: selectMyChannelClaims(state),
}); });
export default connect(select)(LivestreamComment); export default connect(select)(LivestreamComment);

View file

@ -1,102 +1,87 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import 'scss/component/_livestream-comment.scss';
import React from 'react';
import { parseURI } from 'util/lbryURI';
import MarkdownPreview from 'component/common/markdown-preview';
import Tooltip from 'component/common/tooltip';
import ChannelThumbnail from 'component/channelThumbnail';
import { Menu, MenuButton } from '@reach/menu-button'; import { Menu, MenuButton } from '@reach/menu-button';
import Icon from 'component/common/icon'; import { parseSticker } from 'util/comments';
import { parseURI } from 'util/lbryURI';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames'; import classnames from 'classnames';
import CommentMenuList from 'component/commentMenuList'; import CommentMenuList from 'component/commentMenuList';
import Button from 'component/button';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import Icon from 'component/common/icon';
import MarkdownPreview from 'component/common/markdown-preview';
import OptimizedImage from 'component/optimizedImage'; import OptimizedImage from 'component/optimizedImage';
import { parseSticker } from 'util/comments'; import React from 'react';
import Tooltip from 'component/common/tooltip';
type Props = { type Props = {
uri: string,
claim: StreamClaim, claim: StreamClaim,
authorUri: string, comment: Comment,
commentId: string, myChannels: ?Array<ChannelClaim>,
message: string,
commentIsMine: boolean,
stakedLevel: number, stakedLevel: number,
supportAmount: number, uri: string,
isModerator: boolean,
isGlobalMod: boolean,
isFiat: boolean,
isPinned: boolean,
}; };
function LivestreamComment(props: Props) { function LivestreamComment(props: Props) {
const { const { claim, comment, myChannels, stakedLevel, uri } = props;
claim,
uri,
authorUri,
message,
commentIsMine,
commentId,
stakedLevel,
supportAmount,
isModerator,
isGlobalMod,
isFiat,
isPinned,
} = props;
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri; const {
const { claimName } = parseURI(authorUri); channel_url: authorUri,
channel_id: authorId,
comment_id: commentId,
comment: message,
is_fiat: isFiat,
is_global_mod: isGlobalMod,
is_moderator: isModerator,
is_pinned: isPinned,
support_amount: supportAmount,
} = comment;
const commentIsMine = authorId && myChannels && myChannels.some(({ claim_id }) => claim_id === authorId);
const commentByContentOwner = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
const stickerFromMessage = parseSticker(message); const stickerFromMessage = parseSticker(message);
let claimName;
try {
authorUri && ({ claimName } = parseURI(authorUri));
} catch (e) {}
const commentBadge = (label: string, icon: string, className?: string) => (
<Tooltip label={label}>
<span className={classnames('comment__badge', { className })}>
<Icon icon={icon} size={16} />
</span>
</Tooltip>
);
return ( return (
<li <li
className={classnames('livestream-comment', { className={classnames('livestreamComment', {
'livestream-comment--superchat': supportAmount > 0, 'livestreamComment--superchat': supportAmount > 0,
'livestream-comment--sticker': Boolean(stickerFromMessage), 'livestreamComment--sticker': Boolean(stickerFromMessage),
})} })}
> >
{supportAmount > 0 && ( {supportAmount > 0 && (
<div className="super-chat livestream-superchat__banner"> <div className="superChat livestreamComment__superchatBanner">
<div className="livestream-superchat__banner-corner" /> <div className="livestreamComment__superchatBanner-corner" />
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestream-superchat__amount" /> <CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestreamSuperchat__amount" />
</div> </div>
)} )}
<div className="livestream-comment__body"> <div className="livestreamComment__body">
{(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />} {(supportAmount > 0 || Boolean(stickerFromMessage)) && <ChannelThumbnail uri={authorUri} xsmall />}
<div <div
className={classnames('livestream-comment__info', { className={classnames('livestreamComment__info', {
'livestream-comment__info--sticker': Boolean(stickerFromMessage), 'livestreamComment__info--sticker': Boolean(stickerFromMessage),
})} })}
> >
{isGlobalMod && ( {isGlobalMod && commentBadge(__('Admin'), ICONS.BADGE_MOD, 'comment__badge--globalMod')}
<Tooltip label={__('Admin')}> {isModerator && commentBadge(__('Moderator'), ICONS.BADGE_MOD, 'comment__badge--mod')}
<span className="comment__badge comment__badge--global-mod"> {commentByContentOwner && commentBadge(__('Streamer'), ICONS.BADGE_STREAMER)}
<Icon icon={ICONS.BADGE_MOD} size={16} />
</span>
</Tooltip>
)}
{isModerator && (
<Tooltip label={__('Moderator')}>
<span className="comment__badge comment__badge--mod">
<Icon icon={ICONS.BADGE_MOD} size={16} />
</span>
</Tooltip>
)}
{commentByOwnerOfContent && (
<Tooltip label={__('Streamer')}>
<span className="comment__badge">
<Icon icon={ICONS.BADGE_STREAMER} size={16} />
</span>
</Tooltip>
)}
<Button <Button
className={classnames('button--uri-indicator comment__author', { className={classnames('button--uri-indicator comment__author', {
'comment__author--creator': commentByOwnerOfContent, 'comment__author--creator': commentByContentOwner,
})} })}
target="_blank" target="_blank"
navigate={authorUri} navigate={authorUri}
@ -112,18 +97,18 @@ function LivestreamComment(props: Props) {
)} )}
{stickerFromMessage ? ( {stickerFromMessage ? (
<div className="sticker__comment"> <div className="comment__message--sticker">
<OptimizedImage src={stickerFromMessage.url} waitLoad /> <OptimizedImage src={stickerFromMessage.url} waitLoad />
</div> </div>
) : ( ) : (
<div className="livestream-comment__text"> <div className="livestreamComment__text">
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps /> <MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} disableTimestamps />
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="livestream-comment__menu"> <div className="livestreamComment__menu">
<Menu> <Menu>
<MenuButton className="menu__button"> <MenuButton className="menu__button">
<Icon size={18} icon={ICONS.MORE_VERTICAL} /> <Icon size={18} icon={ICONS.MORE_VERTICAL} />

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectClaimForUri, selectMyChannelClaims } from 'redux/selectors/claims'; import { selectClaimForUri } from 'redux/selectors/claims';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket'; import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { doCommentList, doSuperChatList } from 'redux/actions/comments'; import { doCommentList, doSuperChatList } from 'redux/actions/comments';
import { import {
@ -20,7 +20,6 @@ const select = (state, props) => ({
fetchingComments: selectIsFetchingComments(state), fetchingComments: selectIsFetchingComments(state),
superChats: makeSelectSuperChatsForUri(props.uri)(state), superChats: makeSelectSuperChatsForUri(props.uri)(state),
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state), superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
myChannels: selectMyChannelClaims(state),
}); });
export default connect(select, { export default connect(select, {

View file

@ -1,32 +1,32 @@
// @flow // @flow
import React from 'react'; import 'scss/component/_livestream-comments.scss';
import classnames from 'classnames';
import Spinner from 'component/spinner';
import CommentCreate from 'component/commentCreate';
import LivestreamComment from 'component/livestreamComment';
import Button from 'component/button';
import UriIndicator from 'component/uriIndicator';
import CreditAmount from 'component/common/credit-amount';
import ChannelThumbnail from 'component/channelThumbnail';
import Tooltip from 'component/common/tooltip';
import * as ICONS from 'constants/icons';
import OptimizedImage from 'component/optimizedImage';
import { parseSticker } from 'util/comments'; import { parseSticker } from 'util/comments';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames';
import CommentCreate from 'component/commentCreate';
import CreditAmount from 'component/common/credit-amount';
import LivestreamComment from 'component/livestreamComment';
import OptimizedImage from 'component/optimizedImage';
import React from 'react';
import Spinner from 'component/spinner';
import Tooltip from 'component/common/tooltip';
import UriIndicator from 'component/uriIndicator';
type Props = { type Props = {
uri: string,
claim: ?StreamClaim,
activeViewers: number, activeViewers: number,
claim: ?StreamClaim,
comments: Array<Comment>,
embed?: boolean, embed?: boolean,
fetchingComments: boolean,
pinnedComments: Array<Comment>,
superChats: Array<Comment>,
uri: string,
doCommentList: (string, string, number, number) => void,
doCommentSocketConnect: (string, string) => void, doCommentSocketConnect: (string, string) => void,
doCommentSocketDisconnect: (string) => void, doCommentSocketDisconnect: (string) => void,
doCommentList: (string, string, number, number) => void,
comments: Array<Comment>,
pinnedComments: Array<Comment>,
fetchingComments: boolean,
doSuperChatList: (string) => void, doSuperChatList: (string) => void,
superChats: Array<Comment>,
myChannels: ?Array<ChannelClaim>,
}; };
const VIEW_MODE_CHAT = 'view_chat'; const VIEW_MODE_CHAT = 'view_chat';
@ -36,17 +36,16 @@ const COMMENT_SCROLL_TIMEOUT = 25;
export default function LivestreamComments(props: Props) { export default function LivestreamComments(props: Props) {
const { const {
claim, claim,
uri, comments: commentsByChronologicalOrder,
embed, embed,
fetchingComments,
pinnedComments,
superChats: superChatsByTipAmount,
uri,
doCommentList,
doCommentSocketConnect, doCommentSocketConnect,
doCommentSocketDisconnect, doCommentSocketDisconnect,
comments: commentsByChronologicalOrder,
pinnedComments,
doCommentList,
fetchingComments,
doSuperChatList, doSuperChatList,
myChannels,
superChats: superChatsByTipAmount,
} = props; } = props;
let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats; let superChatsFiatAmount, superChatsLBCAmount, superChatsTotalAmount, hasSuperChats;
@ -55,22 +54,28 @@ export default function LivestreamComments(props: Props) {
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT); const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
const [scrollPos, setScrollPos] = React.useState(0); const [scrollPos, setScrollPos] = React.useState(0);
const [showPinned, setShowPinned] = React.useState(true); const [showPinned, setShowPinned] = React.useState(true);
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length; const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
// which kind of superchat to display, either // which kind of superchat to display, either
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount; const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
const stickerSuperChats = const stickerSuperChats =
superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment))); superChatsByTipAmount && superChatsByTipAmount.filter(({ comment }) => Boolean(parseSticker(comment)));
const discussionElement = document.querySelector('.livestream__comments'); const discussionElement = document.querySelector('.livestream__comments');
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null; const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
let superChatsReversed;
// array of superchats organized by fiat or not first, then support amount
if (superChatsByTipAmount) {
const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount));
// for top to bottom display, oldest superchat on top most recent on bottom
superChatsReversed = clonedSuperchats.sort((a, b) => b.timestamp - a.timestamp);
}
const restoreScrollPos = React.useCallback(() => { const restoreScrollPos = React.useCallback(() => {
if (discussionElement) { if (discussionElement) discussionElement.scrollTop = 0;
discussionElement.scrollTop = 0;
}
}, [discussionElement]); }, [discussionElement]);
React.useEffect(() => { React.useEffect(() => {
@ -139,75 +144,41 @@ export default function LivestreamComments(props: Props) {
hasSuperChats = (superChatsTotalAmount || 0) > 0; hasSuperChats = (superChatsTotalAmount || 0) > 0;
} }
let superChatsReversed;
// array of superchats organized by fiat or not first, then support amount
if (superChatsByTipAmount) {
const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount));
// for top to bottom display, oldest superchat on top most recent on bottom
superChatsReversed = clonedSuperchats.sort((a, b) => {
return b.timestamp - a.timestamp;
});
}
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
function isMyComment(channelId: string) {
if (myChannels != null && channelId != null) {
for (let i = 0; i < myChannels.length; i++) {
if (myChannels[i].claim_id === channelId) {
return true;
}
}
}
return false;
}
if (!claim) {
return null;
}
function getStickerUrl(comment: string) { function getStickerUrl(comment: string) {
const stickerFromComment = parseSticker(comment); const stickerFromComment = parseSticker(comment);
return stickerFromComment && stickerFromComment.url; return stickerFromComment && stickerFromComment.url;
} }
return ( const getChatContentToggle = (toggleMode: string, label: any) => (
<Button
className={classnames('button-toggle', { 'button-toggle--active': viewMode === toggleMode })}
label={label}
onClick={() => {
setViewMode(toggleMode);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
const divHeight = livestreamCommentsDiv.scrollHeight;
livestreamCommentsDiv.scrollTop = toggleMode === VIEW_MODE_CHAT ? divHeight : divHeight * -1;
}}
/>
);
return !claim ? null : (
<div className="card livestream__discussion"> <div className="card livestream__discussion">
<div className="card__header--between livestream-discussion__header"> <div className="card__header--between livestreamDiscussion__header">
<div className="livestream-discussion__title">{__('Live discussion')}</div> <div className="card__title-section--small livestreamDiscussion__title">{__('Live discussion')}</div>
{hasSuperChats && ( {hasSuperChats && (
<div className="recommended-content__toggles"> <div className="recommended-content__toggles">
{/* the superchats in chronological order button */} {/* the superchats in chronological order button */}
<Button {getChatContentToggle(VIEW_MODE_CHAT, __('Chat'))}
className={classnames('button-toggle', {
'button-toggle--active': viewMode === VIEW_MODE_CHAT,
})}
label={__('Chat')}
onClick={() => {
setViewMode(VIEW_MODE_CHAT);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
livestreamCommentsDiv.scrollTop = livestreamCommentsDiv.scrollHeight;
}}
/>
{/* the button to show superchats listed by most to least support amount */} {/* the button to show superchats listed by most to least support amount */}
<Button {getChatContentToggle(
className={classnames('button-toggle', { VIEW_MODE_SUPER_CHAT,
'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT, <>
})} <CreditAmount amount={superChatsTotalAmount || 0} size={8} /> /
label={ <CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
<> </>
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> / )}
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
</>
}
onClick={() => {
setViewMode(VIEW_MODE_SUPER_CHAT);
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
const divHeight = livestreamCommentsDiv.scrollHeight;
livestreamCommentsDiv.scrollTop = divHeight * -1;
}}
/>
</div> </div>
)} )}
</div> </div>
@ -217,10 +188,10 @@ export default function LivestreamComments(props: Props) {
<Spinner /> <Spinner />
</div> </div>
)} )}
<div ref={commentsRef} className="livestream__comments-wrapper"> <div ref={commentsRef} className="livestreamComments__wrapper">
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && ( {viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && hasSuperChats && (
<div className="livestream-superchats__wrapper"> <div className="livestreamSuperchats__wrapper">
<div className="livestream-superchats__inner"> <div className="livestreamSuperchats__inner">
{superChatsByTipAmount.map((superChat: Comment) => { {superChatsByTipAmount.map((superChat: Comment) => {
const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat); const isSticker = stickerSuperChats && stickerSuperChats.includes(superChat);
@ -230,28 +201,28 @@ export default function LivestreamComments(props: Props) {
return ( return (
<SuperChatWrapper key={superChat.comment_id}> <SuperChatWrapper key={superChat.comment_id}>
<div className="livestream-superchat"> <div className="livestreamSuperchat">
<div className="livestream-superchat__thumbnail"> <div className="livestreamSuperchat__thumbnail">
<ChannelThumbnail uri={superChat.channel_url} xsmall /> <ChannelThumbnail uri={superChat.channel_url} xsmall />
</div> </div>
<div <div
className={classnames('livestream-superchat__info', { className={classnames('livestreamSuperchat__info', {
'livestream-superchat__info--sticker': isSticker, 'livestreamSuperchat__info--sticker': isSticker,
'livestream-superchat__info--not-sticker': stickerSuperChats && !isSticker, 'livestreamSuperchat__info--notSticker': stickerSuperChats && !isSticker,
})} })}
> >
<div className="livestream-superchat__info--user"> <div className="livestreamSuperchat__info--user">
<UriIndicator uri={superChat.channel_url} link /> <UriIndicator uri={superChat.channel_url} link />
<CreditAmount <CreditAmount
size={10} size={10}
className="livestream-superchat__amount-large" className="livestreamSuperchat__amount-large"
amount={superChat.support_amount} amount={superChat.support_amount}
isFiat={superChat.is_fiat} isFiat={superChat.is_fiat}
/> />
</div> </div>
{stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && ( {stickerSuperChats.includes(superChat) && getStickerUrl(superChat.comment) && (
<div className="livestream-superchat__info--image"> <div className="livestreamSuperchat__info--image">
<OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad /> <OptimizedImage src={getStickerUrl(superChat.comment)} waitLoad />
</div> </div>
)} )}
@ -265,20 +236,8 @@ export default function LivestreamComments(props: Props) {
)} )}
{pinnedComment && showPinned && viewMode === VIEW_MODE_CHAT && ( {pinnedComment && showPinned && viewMode === VIEW_MODE_CHAT && (
<div className="livestream-pinned__wrapper"> <div className="livestreamPinned__wrapper">
<LivestreamComment <LivestreamComment key={pinnedComment.comment_id} uri={uri} comment={pinnedComment} />
key={pinnedComment.comment_id}
uri={uri}
authorUri={pinnedComment.channel_url}
commentId={pinnedComment.comment_id}
message={pinnedComment.comment}
supportAmount={pinnedComment.support_amount}
isModerator={pinnedComment.is_moderator}
isGlobalMod={pinnedComment.is_global_mod}
isFiat={pinnedComment.is_fiat}
isPinned={pinnedComment.is_pinned}
commentIsMine={pinnedComment.channel_id && isMyComment(pinnedComment.channel_id)}
/>
<Button <Button
title={__('Dismiss pinned comment')} title={__('Dismiss pinned comment')}
button="inverse" button="inverse"
@ -294,36 +253,14 @@ export default function LivestreamComments(props: Props) {
<div className="livestream__comments"> <div className="livestream__comments">
{viewMode === VIEW_MODE_CHAT && {viewMode === VIEW_MODE_CHAT &&
commentsToDisplay.map((comment) => ( commentsToDisplay.map((comment) => (
<LivestreamComment <LivestreamComment key={comment.comment_id} uri={uri} comment={comment} />
key={comment.comment_id}
uri={uri}
authorUri={comment.channel_url}
commentId={comment.comment_id}
message={comment.comment}
supportAmount={comment.support_amount}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
/>
))} ))}
{/* listing comments on top of eachother */} {/* listing comments on top of eachother */}
{viewMode === VIEW_MODE_SUPER_CHAT && {viewMode === VIEW_MODE_SUPER_CHAT &&
superChatsReversed && superChatsReversed &&
superChatsReversed.map((comment) => ( superChatsReversed.map((comment) => (
<LivestreamComment <LivestreamComment key={comment.comment_id} uri={uri} comment={comment} />
key={comment.comment_id}
uri={uri}
authorUri={comment.channel_url}
commentId={comment.comment_id}
message={comment.comment}
supportAmount={comment.support_amount}
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
/>
))} ))}
</div> </div>
) : ( ) : (
@ -333,14 +270,14 @@ export default function LivestreamComments(props: Props) {
{scrollPos < 0 && viewMode === VIEW_MODE_CHAT && ( {scrollPos < 0 && viewMode === VIEW_MODE_CHAT && (
<Button <Button
button="secondary" button="secondary"
className="livestream__comments__scroll-to-recent" className="livestreamComments__scrollToRecent"
label={__('Recent Comments')} label={__('Recent Comments')}
onClick={restoreScrollPos} onClick={restoreScrollPos}
iconRight={ICONS.DOWN} iconRight={ICONS.DOWN}
/> />
)} )}
<div className="livestream__comment-create"> <div className="livestream__commentCreate">
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} /> <CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
</div> </div>
</div> </div>

View file

@ -1,11 +1,7 @@
// @flow // @flow
import React from 'react';
import { getThumbnailCdnUrl } from 'util/thumbnail'; import { getThumbnailCdnUrl } from 'util/thumbnail';
import { scaleToDevicePixelRatio } from 'util/scale';
function scaleToDevicePixelRatio(value: number, window: any) { import React from 'react';
const devicePixelRatio = window.devicePixelRatio || 1.0;
return Math.ceil(value * devicePixelRatio);
}
type Props = { type Props = {
src: string, src: string,
@ -40,8 +36,8 @@ function OptimizedImage(props: Props) {
let width = elem.parentElement.clientWidth; let width = elem.parentElement.clientWidth;
let height = elem.parentElement.clientHeight; let height = elem.parentElement.clientHeight;
width = scaleToDevicePixelRatio(width, window); width = scaleToDevicePixelRatio(width);
height = scaleToDevicePixelRatio(height, window); height = scaleToDevicePixelRatio(height);
// Round to next 100px for better caching // Round to next 100px for better caching
width = Math.ceil(width / 100) * 100; width = Math.ceil(width / 100) * 100;

View file

@ -1 +0,0 @@
export const COMMENT_HIGHLIGHTED = 'comment--highlighted';

View file

@ -1,22 +1,22 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims'; import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import { doResolveUri } from 'redux/actions/claims'; import { doClaimSearch } from 'redux/actions/claims';
import { doSetPlayingUri } from 'redux/actions/content'; import { doSetPlayingUri } from 'redux/actions/content';
import { doUserSetReferrer } from 'redux/actions/user'; import { doUserSetReferrer } from 'redux/actions/user';
import { makeSelectTagInClaimOrChannelForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectHasUnclaimedRefereeReward } from 'redux/selectors/rewards';
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import LivestreamPage from './view'; import LivestreamPage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
hasUnclaimedRefereeReward: selectHasUnclaimedRefereeReward(state),
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
channelClaim: makeSelectClaimForUri(props.uri)(state), channelClaim: makeSelectClaimForUri(props.uri)(state),
chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state), chatDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
}); });
export default connect(select, { const perform = (dispatch) => ({
doSetPlayingUri, clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
doResolveUri, doClaimSearch: (options, cb) => dispatch(doClaimSearch(options, cb)),
doUserSetReferrer, setReferrer: (referrer) => dispatch(doUserSetReferrer(referrer)),
})(LivestreamPage); });
export default connect(select, perform)(LivestreamPage);

View file

@ -1,89 +1,81 @@
// @flow // @flow
import { LIVESTREAM_LIVE_API } from 'constants/livestream'; import { LIVESTREAM_LIVE_API } from 'constants/livestream';
import React from 'react';
import Page from 'component/page';
import LivestreamLayout from 'component/livestreamLayout';
import LivestreamComments from 'component/livestreamComments';
import analytics from 'analytics'; import analytics from 'analytics';
import Lbry from 'lbry'; import LivestreamComments from 'component/livestreamComments';
import LivestreamLayout from 'component/livestreamLayout';
import Page from 'component/page';
import React from 'react';
const STREAMING_POLL_INTERVAL_IN_MS = 10000;
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000;
type Props = { type Props = {
uri: string,
claim: StreamClaim,
doSetPlayingUri: ({ uri: ?string }) => void,
isAuthenticated: boolean,
doUserSetReferrer: (string) => void,
channelClaim: ChannelClaim, channelClaim: ChannelClaim,
chatDisabled: boolean, chatDisabled: boolean,
claim: StreamClaim,
isAuthenticated: boolean,
uri: string,
clearPlayingUri: () => void,
doClaimSearch: (any, (ClaimSearchResponse) => void) => void,
setReferrer: (string) => void,
}; };
export default function LivestreamPage(props: Props) { export default function LivestreamPage(props: Props) {
const { uri, claim, doSetPlayingUri, isAuthenticated, doUserSetReferrer, channelClaim, chatDisabled } = props; const {
channelClaim,
chatDisabled,
claim,
isAuthenticated,
uri,
clearPlayingUri,
doClaimSearch,
setReferrer,
} = props;
const [isLive, setIsLive] = React.useState(false); const [isLive, setIsLive] = React.useState(false);
const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false); const [hasLivestreamClaim, setHasLivestreamClaim] = React.useState(false);
const STREAMING_POLL_INTERVAL_IN_MS = 10000; const livestreamChannelId = channelClaim && channelClaim.signing_channel && channelClaim.signing_channel.claim_id;
const LIVESTREAM_CLAIM_POLL_IN_MS = 60000; const stringifiedClaim = JSON.stringify(claim);
React.useEffect(() => { React.useEffect(() => {
let checkClaimsInterval; let checkClaimsInterval;
function checkHasLivestreamClaim() { function checkHasLivestreamClaim() {
Lbry.claim_search({ doClaimSearch({ channel_ids: [livestreamChannelId], has_no_source: true, claim_type: ['stream'] }, (data) =>
channel_ids: [livestreamChannelId], data && data.items && data.items.length > 0 ? setHasLivestreamClaim(true) : undefined
has_no_source: true, );
claim_type: ['stream'],
})
.then((res) => {
if (res && res.items && res.items.length > 0) {
setHasLivestreamClaim(true);
}
})
.catch(() => {});
} }
if (livestreamChannelId && !isLive) { if (livestreamChannelId && !isLive) {
if (!checkClaimsInterval) checkHasLivestreamClaim(); if (!checkClaimsInterval) checkHasLivestreamClaim();
checkClaimsInterval = setInterval(checkHasLivestreamClaim, LIVESTREAM_CLAIM_POLL_IN_MS); checkClaimsInterval = setInterval(checkHasLivestreamClaim, LIVESTREAM_CLAIM_POLL_IN_MS);
return () => { return () => (checkClaimsInterval ? clearInterval(checkClaimsInterval) : undefined);
if (checkClaimsInterval) {
clearInterval(checkClaimsInterval);
}
};
} }
}, [livestreamChannelId, isLive]); }, [livestreamChannelId, isLive, doClaimSearch]);
React.useEffect(() => { React.useEffect(() => {
let interval; let interval;
function checkIsLive() { function checkIsLive() {
// TODO: duplicate code below // TODO: duplicate code below
// $FlowFixMe livestream API can handle garbage // $FlowFixMe livestream API can handle garbage
fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`) fetch(`${LIVESTREAM_LIVE_API}/${livestreamChannelId}`)
.then((res) => res.json()) .then((res) => res.json())
.then((res) => { .then((res) =>
if (!res || !res.data) { !res || !res.data ? setIsLive(false) : res.data.hasOwnProperty('live') && setIsLive(res.data.live)
setIsLive(false); );
return;
}
if (res.data.hasOwnProperty('live')) {
setIsLive(res.data.live);
}
});
} }
if (livestreamChannelId && hasLivestreamClaim) { if (livestreamChannelId && hasLivestreamClaim) {
if (!interval) checkIsLive(); if (!interval) checkIsLive();
interval = setInterval(checkIsLive, STREAMING_POLL_INTERVAL_IN_MS); interval = setInterval(checkIsLive, STREAMING_POLL_INTERVAL_IN_MS);
return () => { return () => (interval ? clearInterval(interval) : undefined);
if (interval) {
clearInterval(interval);
}
};
} }
}, [livestreamChannelId, hasLivestreamClaim]); }, [livestreamChannelId, hasLivestreamClaim]);
const stringifiedClaim = JSON.stringify(claim);
React.useEffect(() => { React.useEffect(() => {
if (uri && stringifiedClaim) { if (uri && stringifiedClaim) {
const jsonClaim = JSON.parse(stringifiedClaim); const jsonClaim = JSON.parse(stringifiedClaim);
@ -97,18 +89,16 @@ export default function LivestreamPage(props: Props) {
if (!isAuthenticated) { if (!isAuthenticated) {
const uri = jsonClaim.signing_channel && jsonClaim.signing_channel.permanent_url; const uri = jsonClaim.signing_channel && jsonClaim.signing_channel.permanent_url;
if (uri) { if (uri) setReferrer(uri.replace('lbry://', ''));
doUserSetReferrer(uri.replace('lbry://', ''));
}
} }
} }
}, [uri, stringifiedClaim, isAuthenticated]); }, [uri, stringifiedClaim, isAuthenticated, setReferrer]);
React.useEffect(() => { React.useEffect(() => {
// Set playing uri to null so the popout player doesnt start playing the dummy claim if a user navigates back // Set playing uri to null so the popout player doesnt start playing the dummy claim if a user navigates back
// This can be removed when we start using the app video player, not a LIVESTREAM iframe // This can be removed when we start using the app video player, not a LIVESTREAM iframe
doSetPlayingUri({ uri: null }); clearPlayingUri();
}, [doSetPlayingUri]); }, [clearPlayingUri]);
return ( return (
<Page <Page

View file

@ -1,14 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { import {
selectIsFetchingComments, selectIsFetchingComments,
selectCommentsForUri, selectCommentsForUri,
makeSelectTotalCommentsCountForUri, makeSelectTotalCommentsCountForUri,
makeSelectTopLevelTotalPagesForUri, makeSelectTopLevelTotalPagesForUri,
} from 'redux/selectors/comments'; } from 'redux/selectors/comments';
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { selectClaimsById } from 'redux/selectors/claims'; import { selectClaimsById } from 'redux/selectors/claims';
import OwnComments from './view'; import OwnComments from './view';
const select = (state) => { const select = (state) => {
@ -18,16 +17,16 @@ const select = (state) => {
return { return {
activeChannelClaim, activeChannelClaim,
allComments: selectCommentsForUri(state, uri), allComments: selectCommentsForUri(state, uri),
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
isFetchingComments: selectIsFetchingComments(state),
claimsById: selectClaimsById(state), claimsById: selectClaimsById(state),
isFetchingComments: selectIsFetchingComments(state),
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
}; };
}; };
const perform = (dispatch) => ({ const perform = (dispatch) => ({
doCommentReset: (a) => dispatch(doCommentReset(a)), doCommentListOwn: (channelId, page, pageSize) => dispatch(doCommentListOwn(channelId, page, pageSize)),
doCommentListOwn: (a, b, c) => dispatch(doCommentListOwn(a, b, c)), doCommentReset: (claimId) => dispatch(doCommentReset(claimId)),
}); });
export default connect(select, perform)(OwnComments); export default connect(select, perform)(OwnComments);

View file

@ -1,25 +1,20 @@
// @flow // @flow
import React from 'react'; import 'scss/component/_comments-list.scss';
import 'scss/component/_comments-own.scss';
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
import { scaleToDevicePixelRatio } from 'util/scale';
import * as ICONS from 'constants/icons';
import Button from 'component/button'; import Button from 'component/button';
import Card from 'component/common/card';
import ChannelSelector from 'component/channelSelector'; import ChannelSelector from 'component/channelSelector';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import Comment from 'component/comment'; import Comment from 'component/comment';
import Card from 'component/common/card'; import debounce from 'util/debounce';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import Page from 'component/page'; import Page from 'component/page';
import React from 'react';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
import * as ICONS from 'constants/icons';
import useFetched from 'effects/use-fetched'; import useFetched from 'effects/use-fetched';
import debounce from 'util/debounce';
function scaleToDevicePixelRatio(value) {
const devicePixelRatio = window.devicePixelRatio || 1.0;
if (devicePixelRatio < 1.0) {
return Math.ceil(value / devicePixelRatio);
}
return Math.ceil(value * devicePixelRatio);
}
type Props = { type Props = {
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
@ -42,6 +37,7 @@ export default function OwnComments(props: Props) {
doCommentReset, doCommentReset,
doCommentListOwn, doCommentListOwn,
} = props; } = props;
const spinnerRef = React.useRef(); const spinnerRef = React.useRef();
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [activeChannelId, setActiveChannelId] = React.useState(''); const [activeChannelId, setActiveChannelId] = React.useState('');
@ -53,51 +49,6 @@ export default function OwnComments(props: Props) {
const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL); const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL);
const moreBelow = page < totalPages; const moreBelow = page < totalPages;
function getCommentsElem(comments) {
return comments.map((comment) => {
const contentClaim = claimsById[comment.claim_id];
const isChannel = contentClaim && contentClaim.value_type === 'channel';
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
return (
<div key={comment.comment_id} className="comments-own card__main-actions">
<div className="section__actions">
<div className="comments-own--claim">
{contentClaim && (
<ClaimPreview
uri={contentClaim.canonical_url}
searchParams={{
...(isChannel ? { view: 'discussion' } : {}),
...(isLivestream ? {} : { lc: comment.comment_id }),
}}
hideActions
hideMenu
properties={() => null}
/>
)}
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
</div>
<Comment
isTopLevel
hideActions
authorUri={comment.channel_url}
author={comment.channel_name}
commentId={comment.comment_id}
message={comment.comment}
timePosted={comment.timestamp * 1000}
commentIsMine
supportAmount={comment.support_amount}
numDirectReplies={0} // Don't show replies here
isModerator={comment.is_moderator}
isGlobalMod={comment.is_global_mod}
isFiat={comment.is_fiat}
/>
</div>
</div>
);
});
}
// Active channel changed // Active channel changed
React.useEffect(() => { React.useEffect(() => {
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) { if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
@ -166,34 +117,49 @@ export default function OwnComments(props: Props) {
// ************************************************************************** // **************************************************************************
// ************************************************************************** // **************************************************************************
if (!activeChannelClaim) { const getCommentsElem = (comments: Array<Comment>) =>
return null; comments.map((comment: Comment) => {
} const contentClaim = claimsById[comment.claim_id];
const isChannel = contentClaim && contentClaim.value_type === 'channel';
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
return ( return (
<div key={comment.comment_id} className="comments--own card__main-actions">
<div className="section__actions">
<div className="comments--own__claim">
{contentClaim && (
<ClaimPreview
uri={contentClaim.canonical_url}
searchParams={{
...(isChannel ? { view: 'discussion' } : {}),
...(isLivestream ? {} : { lc: comment.comment_id }),
}}
hideActions
hideMenu
properties={() => null}
/>
)}
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
</div>
{/* Don't show replies here */}
<Comment numDirectReplies={0} comment={comment} isTopLevel hideActions commentIsMine />
</div>
</div>
);
});
return !activeChannelClaim ? null : (
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}> <Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}>
<ChannelSelector hideAnon /> <ChannelSelector hideAnon />
<Card <Card
isBodyList isBodyList
title={ title={
totalComments > 0 (isFetchingComments && '') ||
? totalComments === 1 (totalComments > 0 && __('No comments')) ||
? __('1 comment') (totalComments === 1 && __('1 comment')) ||
: __('%total_comments% comments', { total_comments: totalComments }) (totalComments > 1 && __('%total_comments% comments', { total_comments: totalComments }))
: isFetchingComments
? ''
: __('No comments')
}
titleActions={
<Button
button="alt"
icon={ICONS.REFRESH}
title={__('Refresh')}
onClick={() => {
setPage(0);
}}
/>
} }
titleActions={<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />}
body={ body={
<> <>
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>} {wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}

View file

@ -24,6 +24,21 @@ import { doFetchItemsInCollections } from 'redux/actions/collections';
let onChannelConfirmCallback; let onChannelConfirmCallback;
let checkPendingInterval; let checkPendingInterval;
type ClaimSearchParams = {
page_size?: number,
page: number,
no_totals?: boolean,
any_tags?: Array<string>,
claim_ids?: Array<string>,
channel_ids?: Array<string>,
not_channel_ids?: Array<string>,
not_tags?: Array<string>,
order_by?: Array<string>,
release_time?: string,
has_source?: boolean,
has_no_souce?: boolean,
};
export function doResolveUris( export function doResolveUris(
uris: Array<string>, uris: Array<string>,
returnCachedClaims: boolean = false, returnCachedClaims: boolean = false,
@ -604,24 +619,8 @@ export function doFetchCollectionListMine(page: number = 1, pageSize: number = 9
} }
export function doClaimSearch( export function doClaimSearch(
options: { options: ClaimSearchParams = { page_size: 10, page: 1, no_totals: true },
page_size?: number, successCb?: (ClaimSearchResponse) => void
page: number,
no_totals?: boolean,
any_tags?: Array<string>,
claim_ids?: Array<string>,
channel_ids?: Array<string>,
not_channel_ids?: Array<string>,
not_tags?: Array<string>,
order_by?: Array<string>,
release_time?: string,
has_source?: boolean,
has_no_souce?: boolean,
} = {
no_totals: true,
page_size: 10,
page: 1,
}
) { ) {
const query = createNormalizedClaimSearchKey(options); const query = createNormalizedClaimSearchKey(options);
return async (dispatch: Dispatch) => { return async (dispatch: Dispatch) => {
@ -648,6 +647,9 @@ export function doClaimSearch(
pageSize: options.page_size, pageSize: options.page_size,
}, },
}); });
if (successCb) successCb(data);
return resolveInfo; return resolveInfo;
}; };

View file

@ -758,67 +758,53 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
export function doCommentUpdate(comment_id: string, comment: string) { export function doCommentUpdate(comment_id: string, comment: string) {
// if they provided an empty string, they must have wanted to abandon // if they provided an empty string, they must have wanted to abandon
if (comment === '') { if (comment === '') return doCommentAbandon(comment_id);
return doCommentAbandon(comment_id);
} else {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const activeChannelClaim = selectActiveChannelClaim(state); return async (dispatch: Dispatch, getState: GetState) => {
if (!activeChannelClaim) { const state = getState();
return dispatch(doToast({ isError: true, message: __('No active channel selected.') }));
}
const signedComment = await channelSignData(activeChannelClaim.claim_id, comment); const activeChannelClaim = selectActiveChannelClaim(state);
if (!signedComment) { if (!activeChannelClaim) {
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') })); return dispatch(doToast({ isError: true, message: __('No active channel selected.') }));
} }
dispatch({ const signedComment = await channelSignData(activeChannelClaim.claim_id, comment);
type: ACTIONS.COMMENT_UPDATE_STARTED, if (!signedComment) {
}); return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
}
return Comments.comment_edit({ dispatch({ type: ACTIONS.COMMENT_UPDATE_STARTED });
comment_id: comment_id,
comment: comment, return Comments.comment_edit({
signature: signedComment.signature, comment_id: comment_id,
signing_ts: signedComment.signing_ts, comment: comment,
}) signature: signedComment.signature,
.then((result: CommentEditResponse) => { signing_ts: signedComment.signing_ts,
if (result != null) { })
dispatch({ .then((result: CommentEditResponse) => {
type: ACTIONS.COMMENT_UPDATE_COMPLETED, if (result != null) {
data: { dispatch({ type: ACTIONS.COMMENT_UPDATE_COMPLETED, data: { comment: result } });
comment: result, } else {
}, // the result will return null
}); dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED });
} else {
// the result will return null
dispatch({
type: ACTIONS.COMMENT_UPDATE_FAILED,
});
dispatch(
doToast({
message: 'Your channel is still being setup, try again in a few moments.',
isError: true,
})
);
}
})
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_UPDATE_FAILED,
data: error,
});
dispatch( dispatch(
doToast({ doToast({
message: 'Unable to edit this comment, please try again later.', message: 'Your channel is still being setup, try again in a few moments.',
isError: true, isError: true,
}) })
); );
}); }
}; })
} .catch((error) => {
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED, data: error });
dispatch(
doToast({
message: 'Unable to edit this comment, please try again later.',
isError: true,
})
);
});
};
} }
async function channelSignName(channelClaimId: string, channelName: string) { async function channelSignName(channelClaimId: string, channelName: string) {

View file

@ -17,7 +17,6 @@
@import 'component/channel-mention'; @import 'component/channel-mention';
@import 'component/claim-list'; @import 'component/claim-list';
@import 'component/collection'; @import 'component/collection';
@import 'component/comments';
@import 'component/content'; @import 'component/content';
@import 'component/dat-gui'; @import 'component/dat-gui';
@import 'component/embed-player'; @import 'component/embed-player';

View file

@ -1,3 +1,5 @@
@import '../../scss/component/form-field';
.button { .button {
display: inline-block; display: inline-block;
position: relative; position: relative;

View 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;
}
}

View 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);
}
}
}

View file

@ -1,37 +1,17 @@
@import '../../../web/scss/themes/odysee/init/vars';
@import '../../scss/component/button';
@import '../../scss/init/gui';
@import '../../scss/init/mixins';
$thumbnailWidth: 1.5rem; $thumbnailWidth: 1.5rem;
$thumbnailWidthSmall: 1rem; $thumbnailWidthSmall: 1rem;
.comments {
list-style-type: none;
font-size: var(--font-small);
margin-top: var(--spacing-l);
}
.comments--contracted {
@extend .comments;
max-height: 5rem;
overflow: hidden;
-webkit-mask-image: -webkit-gradient(linear, left 30%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
overflow-wrap: anywhere;
}
.comments--replies { .comments--replies {
list-style-type: none; list-style-type: none;
margin-left: var(--spacing-s); margin-left: var(--spacing-s);
flex: 1; flex: 1;
} }
.comment__sort {
margin: var(--spacing-s) 0;
margin-right: var(--spacing-s);
display: block;
@media (min-width: $breakpoint-small) {
margin-top: 0;
display: inline;
}
}
.comment { .comment {
width: 100%; width: 100%;
display: flex; display: flex;
@ -44,7 +24,7 @@ $thumbnailWidthSmall: 1rem;
margin-top: var(--spacing-l); margin-top: var(--spacing-l);
} }
.comment__author-thumbnail { .commentAuthor__thumbnail {
@include handleChannelGif($thumbnailWidthSmall); @include handleChannelGif($thumbnailWidthSmall);
margin-right: 0; margin-right: 0;
@ -67,7 +47,7 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment__thumbnail-wrapper { .commentThumbnail__wrapper {
flex: 0; flex: 0;
margin-top: var(--spacing-xxs); margin-top: var(--spacing-xxs);
} }
@ -77,7 +57,7 @@ $thumbnailWidthSmall: 1rem;
flex-direction: row; flex-direction: row;
} }
.comment__replies-container { .commentReplies__container {
margin: 0; margin: 0;
} }
@ -99,17 +79,17 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment--top-level { .comment--topLevel {
&:not(:first-child) { &:not(:first-child) {
margin-top: var(--spacing-l); margin-top: var(--spacing-l);
} }
} }
.comment--slimed { .comment__content--slimed {
opacity: 0.6; opacity: 0.6;
} }
.comment__edit-input { .commentBody__editInput {
margin-top: var(--spacing-xxs); margin-top: var(--spacing-xxs);
} }
@ -132,13 +112,13 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment--highlighted { .comment__content--highlighted {
background: var(--color-comment-highlighted); background: var(--color-comment-highlighted);
box-shadow: 0 0 0 5px var(--color-comment-highlighted); box-shadow: 0 0 0 5px var(--color-comment-highlighted);
border-radius: 4px; border-radius: 4px;
} }
.comment__body-container { .commentBody__container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
@ -164,7 +144,7 @@ $thumbnailWidthSmall: 1rem;
justify-content: space-between; justify-content: space-between;
} }
.comment__meta-information { .commentMeta__information {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
@ -188,14 +168,14 @@ $thumbnailWidthSmall: 1rem;
} }
} }
.comment__badge--global-mod { .commentBadge__globalMod {
.st0 { .st0 {
// @see: ICONS.BADGE_MOD // @see: ICONS.BADGE_MOD
fill: #fe7500; fill: #fe7500;
} }
} }
.comment__badge--mod { .commentBadge__mod {
.st0 { .st0 {
// @see: ICONS.BADGE_MOD // @see: ICONS.BADGE_MOD
fill: #ff3850; fill: #ff3850;
@ -422,64 +402,7 @@ $thumbnailWidthSmall: 1rem;
opacity: 0.5; opacity: 0.5;
} }
.comments-own { .comment__message--sticker {
.section__actions {
align-items: flex-start;
}
.comments-own--claim {
min-width: 100%;
max-width: 100%;
@media (min-width: $breakpoint-medium) {
min-width: 40%;
max-width: 40%;
}
.media__thumb {
flex-shrink: 0;
overflow: hidden;
$width: 5rem;
@include handleClaimListGifThumbnail($width);
width: $width;
height: calc(#{$width} * (9 / 16));
margin-right: var(--spacing-s);
}
.channel-thumbnail {
@include handleChannelGif(calc(5rem * 9 / 16));
margin-right: var(--spacing-xs);
@media (min-width: $breakpoint-small) {
@include handleChannelGif(calc(5rem * 9 / 16));
margin-right: var(--spacing-s);
}
}
}
.claim-preview__wrapper {
margin: 0 0;
padding: 0;
@media (min-width: $breakpoint-medium) {
margin: 0 var(--spacing-xs);
}
}
.comment {
margin-top: var(--spacing-s);
margin-left: 0;
padding-left: var(--spacing-m);
border-left: 4px solid var(--color-border);
@media (min-width: $breakpoint-medium) {
margin-top: 0;
margin-left: var(--spacing-s);
}
}
}
.sticker__comment {
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
height: 6rem; height: 6rem;
overflow: hidden; overflow: hidden;

View 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);
}
}
}

View 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;
}

View file

@ -26,162 +26,6 @@ $recent-msg-button__height: 2rem;
} }
} }
.livestream__discussion {
width: 100%;
@media (min-width: $breakpoint-medium) {
margin: 0;
width: var(--livestream-comments-width);
height: calc(100vh - var(--header-height));
position: fixed;
right: 0;
top: var(--header-height);
bottom: 0;
border-radius: 0;
border-top: none;
border-bottom: none;
border-right: none;
.card__main-actions {
padding: 0;
}
}
}
.livestream-discussion__header {
border-bottom: 1px solid var(--color-border);
padding-bottom: var(--spacing-s);
margin-bottom: 0;
align-items: center;
@media (min-width: $breakpoint-small) {
height: $discussion-header__height;
padding: 0 var(--spacing-s);
padding-right: 0;
}
}
.livestream-discussion__title {
@extend .card__title-section;
@extend .card__title-section--small;
padding: 0;
}
.livestream__comments-wrapper {
display: flex;
flex-direction: column;
height: calc(100vh - var(--header-height) - #{$discussion-header__height});
}
.livestream__comments {
display: flex;
flex-direction: column-reverse;
font-size: var(--font-small);
overflow-y: scroll;
overflow-x: visible;
padding-top: var(--spacing-s);
width: 100%;
}
.livestream-comment {
list-style-type: none;
position: relative;
@media (min-width: $breakpoint-small) {
&:hover {
background-color: var(--color-card-background-highlighted);
}
&:not(:hover) {
.menu__button:not(:focus):not([aria-expanded='true']) {
opacity: 0;
}
}
}
.channel-name {
font-size: var(--font-xsmall);
}
}
.livestream-comment__info {
overflow: hidden;
}
.livestream-comment--superchat {
background-color: var(--color-card-background-highlighted);
+ .livestream-comment--superchat {
margin-bottom: var(--spacing-xxs);
}
.livestream-comment__body {
display: flex;
align-items: flex-start;
flex-direction: unset;
flex: unset;
}
.livestream-comment__info {
margin-top: calc(var(--spacing-xxs) / 2);
}
&::before {
position: absolute;
left: 0;
height: 100%;
max-height: 4rem;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
width: 5px;
background-color: var(--color-superchat);
content: '';
}
}
.livestream-comment__body {
display: flex;
align-items: flex-start;
margin-left: var(--spacing-s);
overflow: hidden;
.livestream-comment__info--sticker {
display: flex;
margin: var(--spacing-xxs) 0;
}
.channel-thumbnail {
@include handleChannelGif(2rem);
margin-top: var(--spacing-xxs);
flex-shrink: 0;
}
}
.livestream-comment__menu {
position: absolute;
right: var(--spacing-xs);
top: var(--spacing-xs);
}
.livestream__comments__scroll-to-recent {
margin-top: -$recent-msg-button__height;
align-self: center;
margin-bottom: var(--spacing-xs);
font-size: var(--font-xsmall);
padding: var(--spacing-xxs) var(--spacing-s);
opacity: 0.9;
&:hover {
opacity: 1;
}
}
.livestream__comment-create {
padding: var(--spacing-s);
border-top: 1px solid var(--color-border);
margin-top: auto;
}
.livestream__channel-link { .livestream__channel-link {
margin-bottom: var(--spacing-xl); margin-bottom: var(--spacing-xl);
box-shadow: 0 0 0 rgba(246, 72, 83, 0.4); box-shadow: 0 0 0 rgba(246, 72, 83, 0.4);
@ -243,63 +87,7 @@ $recent-msg-button__height: 2rem;
} }
} }
.livestream-superchats__wrapper { .livestreamSuperchat {
flex-shrink: 0;
position: relative;
overflow-x: scroll;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background);
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-pinned__wrapper {
display: flex;
flex-shrink: 0;
position: relative;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background-highlighted);
width: 100%;
.livestream-comment {
width: 100%;
padding-top: var(--spacing-xs);
max-height: 6rem;
overflow-y: scroll;
}
.close-button {
border-left: 1px solid var(--color-border);
padding: 0 calc(var(--spacing-m) - var(--spacing-xs)) 0 var(--spacing-m);
color: var(--color-text-subtitle);
}
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-superchat__amount-large {
.credit-amount {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
}
.livestream-superchats__inner {
display: flex;
}
.livestream-superchat {
display: flex; display: flex;
margin-right: var(--spacing-xs); margin-right: var(--spacing-xs);
padding: var(--spacing-xxs); padding: var(--spacing-xxs);
@ -337,100 +125,6 @@ $recent-msg-button__height: 2rem;
} }
} }
.livestream-superchat__info {
display: flex;
flex-direction: column;
justify-content: center;
font-size: var(--font-xsmall);
}
.livestream-superchat__info--sticker {
display: flex;
align-items: flex-start;
flex-direction: row;
width: 8rem;
height: 3rem;
.livestream-superchat__info--image {
padding-left: var(--spacing-m);
width: 100%;
height: 100%;
}
.button {
margin-top: calc(var(--spacing-xxs) / 2);
}
}
.livestream-superchat__info--not-sticker {
flex-direction: row;
}
.livestream-superchat__banner {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: 0.25rem var(--spacing-s);
display: inline-block;
position: relative;
}
// This is just a two small circles that overlap to make it look like
// the banner and the left border are connected
.livestream-superchat__banner-corner {
height: calc(var(--border-radius) * 2);
width: calc(var(--border-radius) * 2);
border-radius: 50%;
position: absolute;
background-color: var(--color-superchat);
bottom: 0;
left: 0;
transform: translateX(0) translateY(50%);
&::after {
content: '';
height: calc(var(--border-radius) * 2);
width: calc(var(--border-radius) * 2);
border-top-left-radius: var(--border-radius);
background-color: var(--color-card-background);
position: absolute;
bottom: 0;
left: 0;
transform: translateX(25%) translateY(50%);
}
}
.livestream-comment__text {
padding-right: var(--spacing-xl);
padding-bottom: var(--spacing-xxs);
.markdown-preview {
p {
word-break: break-word;
}
.channel-name {
font-size: var(--font-small);
}
}
}
.livestream-superchat__tooltip-amount {
margin-top: var(--spacing-xs);
margin-left: 0;
background-color: transparent;
padding: 0;
}
.livestream__superchat-comment {
margin-top: var(--spacing-s);
max-width: 5rem;
overflow-wrap: break-word;
}
.livestream-superchat__amount-large {
min-width: 2.5rem;
}
.table--livestream-data { .table--livestream-data {
td:nth-of-type(1) { td:nth-of-type(1) {
max-width: 4rem; max-width: 4rem;

View file

@ -223,7 +223,7 @@
} }
@media (min-width: $breakpoint-medium) and (max-width: $breakpoint-large + 300px) { @media (min-width: $breakpoint-medium) and (max-width: $breakpoint-large + 300px) {
max-width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 3); max-width: calc(100vw - var(--livestreamComments-width) - var(--spacing-m) * 3);
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
} }
@ -234,7 +234,7 @@
} }
.main__right-side { .main__right-side {
width: var(--livestream-comments-width); width: var(--livestreamComments-width);
@media (max-width: $breakpoint-medium) { @media (max-width: $breakpoint-medium) {
width: 100%; width: 100%;

View file

@ -66,7 +66,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
.super-chat--light { .superChat--light {
position: absolute; position: absolute;
display: inline; display: inline;
bottom: 0; bottom: 0;

View file

@ -1,4 +1,4 @@
.super-chat { .superChat {
border-radius: var(--border-radius); border-radius: var(--border-radius);
background: linear-gradient(to right, var(--color-superchat), var(--color-superchat-3)); background: linear-gradient(to right, var(--color-superchat), var(--color-superchat-3));
padding: 0.2rem var(--spacing-xs); padding: 0.2rem var(--spacing-xs);
@ -10,8 +10,8 @@
} }
} }
.super-chat--light { .superChat--light {
@extend .super-chat; @extend .superChat;
background: linear-gradient(to right, var(--color-superchat__light), var(--color-superchat-3__light)); background: linear-gradient(to right, var(--color-superchat__light), var(--color-superchat-3__light));
.credit-amount { .credit-amount {

View file

@ -247,7 +247,7 @@ textarea {
margin: -1rem 0; margin: -1rem 0;
padding: 0 30px; padding: 0 30px;
background: url('../../static/img/busy.gif') no-repeat center center; background: url('../../../static/img/busy.gif') no-repeat center center;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;

View file

@ -99,7 +99,7 @@ $breakpoint-large: 1600px;
--tag-height: 1.5rem; --tag-height: 1.5rem;
--livestream-comments-width: 30rem; --livestreamComments-width: 30rem;
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {

6
ui/util/scale.js Normal file
View 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);
}

View file

@ -18,7 +18,6 @@
@import '../../ui/scss/component/channel-mention'; @import '../../ui/scss/component/channel-mention';
@import '../../ui/scss/component/claim-list'; @import '../../ui/scss/component/claim-list';
@import '../../ui/scss/component/collection'; @import '../../ui/scss/component/collection';
@import '../../ui/scss/component/comments';
@import '../../ui/scss/component/content'; @import '../../ui/scss/component/content';
@import '../../ui/scss/component/dat-gui'; @import '../../ui/scss/component/dat-gui';
@import '../../ui/scss/component/embed-player'; @import '../../ui/scss/component/embed-player';

View file

@ -18,7 +18,6 @@
@import '../../ui/scss/component/channel-mention'; @import '../../ui/scss/component/channel-mention';
@import '../../ui/scss/component/claim-list'; @import '../../ui/scss/component/claim-list';
@import '../../ui/scss/component/collection'; @import '../../ui/scss/component/collection';
@import '../../ui/scss/component/comments';
@import '../../ui/scss/component/content'; @import '../../ui/scss/component/content';
@import '../../ui/scss/component/dat-gui'; @import '../../ui/scss/component/dat-gui';
@import '../../ui/scss/component/embed-player'; @import '../../ui/scss/component/embed-player';

View file

@ -98,7 +98,7 @@ $breakpoint-large: 1600px;
--tag-height: 1.5rem; --tag-height: 1.5rem;
--livestream-comments-width: 30rem; --livestreamComments-width: 30rem;
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {