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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -758,9 +758,8 @@ export function doCommentAbandon(commentId: string, creatorChannelUri?: string)
export function doCommentUpdate(comment_id: string, comment: string) {
// if they provided an empty string, they must have wanted to abandon
if (comment === '') {
return doCommentAbandon(comment_id);
} else {
if (comment === '') return doCommentAbandon(comment_id);
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
@ -774,9 +773,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
}
dispatch({
type: ACTIONS.COMMENT_UPDATE_STARTED,
});
dispatch({ type: ACTIONS.COMMENT_UPDATE_STARTED });
return Comments.comment_edit({
comment_id: comment_id,
@ -786,17 +783,10 @@ export function doCommentUpdate(comment_id: string, comment: string) {
})
.then((result: CommentEditResponse) => {
if (result != null) {
dispatch({
type: ACTIONS.COMMENT_UPDATE_COMPLETED,
data: {
comment: result,
},
});
dispatch({ type: ACTIONS.COMMENT_UPDATE_COMPLETED, data: { comment: result } });
} else {
// the result will return null
dispatch({
type: ACTIONS.COMMENT_UPDATE_FAILED,
});
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED });
dispatch(
doToast({
message: 'Your channel is still being setup, try again in a few moments.',
@ -806,10 +796,7 @@ export function doCommentUpdate(comment_id: string, comment: string) {
}
})
.catch((error) => {
dispatch({
type: ACTIONS.COMMENT_UPDATE_FAILED,
data: error,
});
dispatch({ type: ACTIONS.COMMENT_UPDATE_FAILED, data: error });
dispatch(
doToast({
message: 'Unable to edit this comment, please try again later.',
@ -819,7 +806,6 @@ export function doCommentUpdate(comment_id: string, comment: string) {
});
};
}
}
async function channelSignName(channelClaimId: string, channelName: string) {
let signedObject;

View file

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

View file

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

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 {
margin-bottom: var(--spacing-xl);
box-shadow: 0 0 0 rgba(246, 72, 83, 0.4);
@ -243,63 +87,7 @@ $recent-msg-button__height: 2rem;
}
}
.livestream-superchats__wrapper {
flex-shrink: 0;
position: relative;
overflow-x: scroll;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background);
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-pinned__wrapper {
display: flex;
flex-shrink: 0;
position: relative;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background-highlighted);
width: 100%;
.livestream-comment {
width: 100%;
padding-top: var(--spacing-xs);
max-height: 6rem;
overflow-y: scroll;
}
.close-button {
border-left: 1px solid var(--color-border);
padding: 0 calc(var(--spacing-m) - var(--spacing-xs)) 0 var(--spacing-m);
color: var(--color-text-subtitle);
}
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-superchat__amount-large {
.credit-amount {
display: flex;
align-items: center;
flex-wrap: nowrap;
}
}
.livestream-superchats__inner {
display: flex;
}
.livestream-superchat {
.livestreamSuperchat {
display: flex;
margin-right: var(--spacing-xs);
padding: var(--spacing-xxs);
@ -337,100 +125,6 @@ $recent-msg-button__height: 2rem;
}
}
.livestream-superchat__info {
display: flex;
flex-direction: column;
justify-content: center;
font-size: var(--font-xsmall);
}
.livestream-superchat__info--sticker {
display: flex;
align-items: flex-start;
flex-direction: row;
width: 8rem;
height: 3rem;
.livestream-superchat__info--image {
padding-left: var(--spacing-m);
width: 100%;
height: 100%;
}
.button {
margin-top: calc(var(--spacing-xxs) / 2);
}
}
.livestream-superchat__info--not-sticker {
flex-direction: row;
}
.livestream-superchat__banner {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: 0.25rem var(--spacing-s);
display: inline-block;
position: relative;
}
// This is just a two small circles that overlap to make it look like
// the banner and the left border are connected
.livestream-superchat__banner-corner {
height: calc(var(--border-radius) * 2);
width: calc(var(--border-radius) * 2);
border-radius: 50%;
position: absolute;
background-color: var(--color-superchat);
bottom: 0;
left: 0;
transform: translateX(0) translateY(50%);
&::after {
content: '';
height: calc(var(--border-radius) * 2);
width: calc(var(--border-radius) * 2);
border-top-left-radius: var(--border-radius);
background-color: var(--color-card-background);
position: absolute;
bottom: 0;
left: 0;
transform: translateX(25%) translateY(50%);
}
}
.livestream-comment__text {
padding-right: var(--spacing-xl);
padding-bottom: var(--spacing-xxs);
.markdown-preview {
p {
word-break: break-word;
}
.channel-name {
font-size: var(--font-small);
}
}
}
.livestream-superchat__tooltip-amount {
margin-top: var(--spacing-xs);
margin-left: 0;
background-color: transparent;
padding: 0;
}
.livestream__superchat-comment {
margin-top: var(--spacing-s);
max-width: 5rem;
overflow-wrap: break-word;
}
.livestream-superchat__amount-large {
min-width: 2.5rem;
}
.table--livestream-data {
td:nth-of-type(1) {
max-width: 4rem;

View file

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

View file

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

View file

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

View file

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

View file

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

6
ui/util/scale.js Normal file
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/claim-list';
@import '../../ui/scss/component/collection';
@import '../../ui/scss/component/comments';
@import '../../ui/scss/component/content';
@import '../../ui/scss/component/dat-gui';
@import '../../ui/scss/component/embed-player';

View file

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

View file

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