new comments
This commit is contained in:
parent
b4106b1a65
commit
248e578422
25 changed files with 750 additions and 194 deletions
25
flow-typed/Comment.js
vendored
Normal file
25
flow-typed/Comment.js
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
declare type Comment = {
|
||||
comment: string, // comment body
|
||||
comment_id: string, // sha256 digest
|
||||
claim_id: string, // id linking to the claim this comment
|
||||
timestamp: number, // integer representing unix-time
|
||||
is_hidden: boolean, // claim owner may enable/disable this
|
||||
channel_id?: string, // claimId of channel signing this comment
|
||||
channel_name?: string, // name of channel claim
|
||||
channel_url?: string, // full lbry url to signing channel
|
||||
signature?: string, // signature of comment by originating channel
|
||||
signing_ts?: string, // timestamp used when signing this comment
|
||||
is_channel_signature_valid?: boolean, // whether or not the signature could be validated
|
||||
parent_id?: number, // comment_id of comment this is in reply to
|
||||
};
|
||||
|
||||
// todo: relate individual comments to their commentId
|
||||
declare type CommentsState = {
|
||||
commentsByUri: { [string]: string },
|
||||
byId: { [string]: Array<string> },
|
||||
repliesByParentId: { [string]: Array<string> }, // ParentCommentID -> list of reply comments
|
||||
topLevelCommentsById: { [string]: Array<string> }, // ClaimID -> list of top level comments
|
||||
commentById: { [string]: Comment },
|
||||
isLoading: boolean,
|
||||
myComments: ?Set<string>,
|
||||
};
|
|
@ -10,6 +10,8 @@ import { doCommentAbandon, doCommentUpdate } from 'redux/actions/comments';
|
|||
import { doToggleBlockChannel } from 'redux/actions/blocked';
|
||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
||||
import Comment from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectIsFetchingComments } from 'redux/selectors/comments';
|
||||
|
||||
const select = (state, props) => ({
|
||||
pending: props.authorUri && makeSelectClaimIsPending(props.authorUri)(state),
|
||||
|
@ -17,6 +19,8 @@ const select = (state, props) => ({
|
|||
isResolvingUri: props.authorUri && makeSelectIsUriResolving(props.authorUri)(state),
|
||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
||||
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -6,13 +6,12 @@ import React, { useEffect, useState } from 'react';
|
|||
import { isEmpty } from 'util/object';
|
||||
import DateTime from 'component/dateTime';
|
||||
import Button from 'component/button';
|
||||
import Expandable from 'component/expandable';
|
||||
// import Expandable from 'component/expandable';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import classnames from 'classnames';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
||||
|
@ -34,6 +33,7 @@ type Props = {
|
|||
updateComment: (string, string) => void,
|
||||
deleteComment: string => void,
|
||||
blockChannel: string => void,
|
||||
linkedComment?: any,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -57,6 +57,7 @@ function Comment(props: Props) {
|
|||
updateComment,
|
||||
deleteComment,
|
||||
blockChannel,
|
||||
linkedComment,
|
||||
} = props;
|
||||
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
|
@ -65,11 +66,7 @@ function Comment(props: Props) {
|
|||
|
||||
// used for controlling the visibility of the menu icon
|
||||
const [mouseIsHovering, setMouseHover] = useState(false);
|
||||
|
||||
// used for controlling visibility of reply comment component
|
||||
const [isReplying, setReplying] = useState(false);
|
||||
|
||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||
|
||||
// to debounce subsequent requests
|
||||
const shouldFetch =
|
||||
|
@ -108,12 +105,15 @@ function Comment(props: Props) {
|
|||
function handleSubmit() {
|
||||
updateComment(commentId, editedMessage);
|
||||
setEditing(false);
|
||||
setReplying(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classnames('comment', { comment__reply: parentId !== null })}
|
||||
className={classnames('comment', {
|
||||
comment__reply: parentId !== null,
|
||||
comment__highlighted: linkedComment && linkedComment.comment_id === commentId,
|
||||
})}
|
||||
id={commentId}
|
||||
onMouseOver={() => setMouseHover(true)}
|
||||
onMouseOut={() => setMouseHover(false)}
|
||||
>
|
||||
|
@ -133,9 +133,16 @@ function Comment(props: Props) {
|
|||
label={author}
|
||||
/>
|
||||
)}
|
||||
<time className="comment__time" dateTime={timePosted}>
|
||||
{DateTime.getTimeAgoStr(timePosted)}
|
||||
</time>
|
||||
{/* // link here */}
|
||||
<Button
|
||||
navigate={`${uri}?lc=${commentId}`}
|
||||
label={
|
||||
<time className="comment__time" dateTime={timePosted}>
|
||||
{DateTime.getTimeAgoStr(timePosted)}
|
||||
</time>
|
||||
}
|
||||
className="button--uri-indicator"
|
||||
/>
|
||||
</div>
|
||||
<div className="comment__menu">
|
||||
<Menu>
|
||||
|
@ -174,8 +181,6 @@ function Comment(props: Props) {
|
|||
value={editedMessage}
|
||||
charCount={charCount}
|
||||
onChange={handleEditMessageChanged}
|
||||
quickActionLabel={!SIMPLE_SITE && (advancedEditor ? __('Simple Editor') : __('Advanced Editor'))}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
<div className="section__actions">
|
||||
|
@ -191,9 +196,9 @@ function Comment(props: Props) {
|
|||
</Form>
|
||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||
<div className="comment__message">
|
||||
<Expandable>
|
||||
<MarkdownPreview content={message} />
|
||||
</Expandable>
|
||||
{/* <Expandable> */}
|
||||
<MarkdownPreview content={message} />
|
||||
{/* </Expandable> */}
|
||||
</div>
|
||||
) : (
|
||||
<div className="comment__message">
|
||||
|
@ -201,27 +206,6 @@ function Comment(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!parentId && !isEditing && (
|
||||
<Button
|
||||
button="link"
|
||||
requiresAuth={IS_WEB}
|
||||
className="comment__reply-button"
|
||||
onClick={() => setReplying(true)}
|
||||
label={__('Reply')}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
{isReplying ? (
|
||||
<CommentCreate
|
||||
uri={uri}
|
||||
parentId={commentId}
|
||||
onDoneReplying={() => setReplying(false)}
|
||||
onCancelReplying={() => setReplying(false)}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ type Props = {
|
|||
parentId?: string,
|
||||
onDoneReplying?: () => void,
|
||||
onCancelReplying?: () => void,
|
||||
isNested: boolean,
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
|
@ -33,6 +34,7 @@ export function CommentCreate(props: Props) {
|
|||
parentId,
|
||||
onDoneReplying,
|
||||
onCancelReplying,
|
||||
isNested,
|
||||
} = props;
|
||||
const { claim_id: claimId } = claim;
|
||||
const isReply = !!parentId;
|
||||
|
@ -53,9 +55,9 @@ export function CommentCreate(props: Props) {
|
|||
useEffect(() => {
|
||||
// set default channel
|
||||
if ((channel === '' || channel === 'anonymous') && topChannel) {
|
||||
handleChannelChange(topChannel.name);
|
||||
setChannel(topChannel.name);
|
||||
}
|
||||
}, [channel, topChannel]);
|
||||
}, [channel, topChannel, setChannel]);
|
||||
|
||||
function handleCommentChange(event) {
|
||||
let commentValue;
|
||||
|
@ -68,10 +70,6 @@ export function CommentCreate(props: Props) {
|
|||
setCommentValue(commentValue);
|
||||
}
|
||||
|
||||
function handleChannelChange(channel) {
|
||||
setChannel(channel);
|
||||
}
|
||||
|
||||
function handleCommentAck() {
|
||||
setCommentAck(true);
|
||||
}
|
||||
|
@ -107,17 +105,27 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} className={classnames('comment__create', { 'comment__create--reply': isReply })}>
|
||||
{!isReply && <ChannelSelection channel={channel} hideAnon onChannelChange={handleChannelChange} />}
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className={classnames('comment__create', {
|
||||
'comment__create--reply': isReply,
|
||||
'comment__create--nested-reply': isNested,
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
disabled={channel === CHANNEL_NEW}
|
||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
label={isReply ? __('Replying as %reply_channel%', { reply_channel: channel }) : __('Comment')}
|
||||
label={
|
||||
<span className="comment-new__label-wrapper">
|
||||
<div className="comment-new__label">{isReply ? __('Replying as') + ' ' : __('Comment as') + ' '}</div>
|
||||
<ChannelSelection channel={channel} hideAnon tiny hideNew onChannelChange={setChannel} />
|
||||
</span>
|
||||
}
|
||||
quickActionLabel={
|
||||
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
||||
}
|
||||
quickActionHandler={!SIMPLE_SITE && (isReply ? undefined : toggleEditorMode)}
|
||||
quickActionHandler={!SIMPLE_SITE && toggleEditorMode}
|
||||
onFocus={onTextareaFocus}
|
||||
placeholder={__('Say something about this...')}
|
||||
value={commentValue}
|
||||
|
@ -126,7 +134,7 @@ export function CommentCreate(props: Props) {
|
|||
autoFocus={isReply}
|
||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||
/>
|
||||
<div className="section__actions">
|
||||
<div className="section__actions section__actions--no-margin">
|
||||
<Button
|
||||
button="primary"
|
||||
disabled={channel === CHANNEL_NEW || !commentValue.length}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
|
||||
import { makeSelectCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments';
|
||||
import { makeSelectTopLevelCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments';
|
||||
import { doCommentList } from 'redux/actions/comments';
|
||||
import CommentsList from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => ({
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
comments: makeSelectCommentsForUri(props.uri)(state),
|
||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
isFetchingComments: selectIsFetchingComments(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Comment from 'component/comment';
|
||||
import Spinner from 'component/spinner';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
import CommentsReplies from 'component/commentsReplies';
|
||||
|
||||
type Props = {
|
||||
comments: Array<any>,
|
||||
|
@ -10,11 +14,28 @@ type Props = {
|
|||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
isFetchingComments: boolean,
|
||||
linkedComment: any,
|
||||
commentingEnabled: boolean,
|
||||
};
|
||||
|
||||
function CommentList(props: Props) {
|
||||
const { fetchComments, uri, comments, claimIsMine, myChannels, isFetchingComments } = props;
|
||||
const {
|
||||
fetchComments,
|
||||
uri,
|
||||
comments,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
isFetchingComments,
|
||||
linkedComment,
|
||||
commentingEnabled,
|
||||
} = props;
|
||||
|
||||
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
||||
const [start] = React.useState(0);
|
||||
const [end, setEnd] = React.useState(9);
|
||||
const totalComments = comments && comments.length;
|
||||
|
||||
const moreBelow = totalComments - end > 0;
|
||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||
const isMyComment = (channelId: string) => {
|
||||
if (myChannels != null && channelId != null) {
|
||||
|
@ -27,56 +48,95 @@ function CommentList(props: Props) {
|
|||
return false;
|
||||
};
|
||||
|
||||
const handleMoreBelow = () => {
|
||||
if (moreBelow) {
|
||||
setEnd(end + 10);
|
||||
}
|
||||
};
|
||||
|
||||
const commentRef = React.useRef();
|
||||
|
||||
useEffect(() => {
|
||||
fetchComments(uri);
|
||||
}, [fetchComments, uri]);
|
||||
|
||||
function sortByParent(arrayOfComments) {
|
||||
let parentComments = arrayOfComments.filter(comment => comment.parent_id === undefined);
|
||||
let childComments = arrayOfComments.filter(comment => comment.parent_id !== undefined);
|
||||
let sortedArray = [];
|
||||
useEffect(() => {
|
||||
if (linkedCommentId && commentRef && commentRef.current) {
|
||||
commentRef.current.scrollIntoView({ block: 'start' });
|
||||
window.scrollBy(0, -100);
|
||||
}
|
||||
}, [linkedCommentId]);
|
||||
|
||||
parentComments.forEach(parentComment => {
|
||||
sortedArray.push(parentComment);
|
||||
function prepareComments(arrayOfComments, linkedComment) {
|
||||
let orderedComments = [];
|
||||
|
||||
childComments
|
||||
.reverse()
|
||||
.filter(childComment => childComment.parent_id === parentComment.comment_id)
|
||||
.forEach(childComment => {
|
||||
sortedArray.push(childComment);
|
||||
});
|
||||
});
|
||||
|
||||
return sortedArray;
|
||||
if (linkedComment) {
|
||||
if (!linkedComment.parent_id) {
|
||||
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.comment_id);
|
||||
orderedComments.unshift(linkedComment);
|
||||
} else {
|
||||
const parentComment = arrayOfComments.find(c => c.comment_id === linkedComment.parent_id);
|
||||
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.parent_id);
|
||||
orderedComments.unshift(parentComment);
|
||||
}
|
||||
} else {
|
||||
orderedComments = arrayOfComments;
|
||||
}
|
||||
return orderedComments;
|
||||
}
|
||||
|
||||
const displayedComments = prepareComments(comments, linkedComment).slice(start, end);
|
||||
|
||||
return (
|
||||
<ul className="comments">
|
||||
{isFetchingComments && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
{!isFetchingComments &&
|
||||
comments &&
|
||||
sortByParent(comments).map(comment => {
|
||||
return (
|
||||
<Comment
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
key={comment.channel_id + comment.comment_id}
|
||||
message={comment.comment}
|
||||
parentId={comment.parent_id || null}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<Card
|
||||
title={<span>{__('Comments')}</span>}
|
||||
actions={
|
||||
<>
|
||||
{commentingEnabled && <CommentCreate uri={uri} />}
|
||||
<ul className="comments" ref={commentRef}>
|
||||
{isFetchingComments && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
{!isFetchingComments &&
|
||||
comments &&
|
||||
displayedComments &&
|
||||
displayedComments.map(comment => {
|
||||
return (
|
||||
<>
|
||||
<Comment
|
||||
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}
|
||||
parentId={comment.parent_id || null}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
/>
|
||||
<CommentsReplies
|
||||
uri={uri}
|
||||
parentId={comment.comment_id}
|
||||
linkedComment={linkedComment}
|
||||
key={comment.comment_id + 'replies'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{moreBelow && (
|
||||
<div className="comment__actions">
|
||||
<Button button="link" className="comment__more-below" onClick={handleMoreBelow} label={__('Show More')} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
15
ui/component/commentsReplies/index.js
Normal file
15
ui/component/commentsReplies/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
|
||||
import { makeSelectRepliesForParentId } from 'redux/selectors/comments';
|
||||
|
||||
import CommentsReplies from './view';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => ({
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
comments: makeSelectRepliesForParentId(props.parentId)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
});
|
||||
|
||||
export default connect(select, null)(CommentsReplies);
|
155
ui/component/commentsReplies/view.jsx
Normal file
155
ui/component/commentsReplies/view.jsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Comment from 'component/comment';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
|
||||
type Props = {
|
||||
comments: Array<any>,
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
linkedComment?: Comment,
|
||||
parentId: string,
|
||||
commentingEnabled: boolean,
|
||||
};
|
||||
|
||||
function CommentsReplies(props: Props) {
|
||||
const { uri, comments, claimIsMine, myChannels, linkedComment, parentId, commentingEnabled } = props;
|
||||
const [isReplying, setReplying] = React.useState(false);
|
||||
const [isExpanded, setExpanded] = React.useState(false);
|
||||
const [start, setStart] = React.useState(0);
|
||||
const [end, setEnd] = React.useState(9);
|
||||
const sortedComments = comments ? [...comments].reverse() : [];
|
||||
const numberOfComments = comments ? comments.length : 0;
|
||||
|
||||
const showMore = () => {
|
||||
if (start > 0) {
|
||||
setStart(0);
|
||||
} else {
|
||||
setEnd(numberOfComments);
|
||||
}
|
||||
};
|
||||
|
||||
const linkedCommentId = linkedComment ? linkedComment.comment_id : '';
|
||||
|
||||
const commentsIndexOfLInked = comments && sortedComments.findIndex(e => e.comment_id === linkedCommentId);
|
||||
|
||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||
const 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;
|
||||
};
|
||||
|
||||
const handleCommentDone = () => {
|
||||
if (!isExpanded) {
|
||||
setExpanded(true);
|
||||
setStart(numberOfComments || 0);
|
||||
}
|
||||
setEnd(numberOfComments + 1);
|
||||
setReplying(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
setStart &&
|
||||
setEnd &&
|
||||
setExpanded &&
|
||||
linkedCommentId &&
|
||||
Number.isInteger(commentsIndexOfLInked) &&
|
||||
commentsIndexOfLInked > -1
|
||||
) {
|
||||
setStart(commentsIndexOfLInked);
|
||||
setEnd(commentsIndexOfLInked + 1);
|
||||
setExpanded(true);
|
||||
}
|
||||
}, [setStart, setEnd, setExpanded, linkedCommentId, commentsIndexOfLInked]);
|
||||
|
||||
const displayedComments = sortedComments.slice(start, end);
|
||||
|
||||
return (
|
||||
<li className={'comment__replies-container'}>
|
||||
<div className="comment__actions">
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Sign in to reply')}
|
||||
className="comment__action"
|
||||
onClick={() => setReplying(!isReplying)}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
{!isExpanded && Boolean(numberOfComments) && (
|
||||
<Button
|
||||
className="comment__action"
|
||||
label={__('Show %number% Replies', { number: numberOfComments })}
|
||||
onClick={() => setExpanded(true)}
|
||||
icon={ICONS.DOWN}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{comments && displayedComments && isExpanded && (
|
||||
<div>
|
||||
<div className="comment__replies">
|
||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||
|
||||
<ul className="comments--replies">
|
||||
{displayedComments.map(comment => {
|
||||
return (
|
||||
<Comment
|
||||
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}
|
||||
parentId={comment.parent_id || null}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedComment={linkedComment}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{!isReplying && (
|
||||
<Button
|
||||
requiresAuth={IS_WEB}
|
||||
label={commentingEnabled ? __('Reply') : __('Sign in to reply')}
|
||||
className="comment__action--nested"
|
||||
onClick={() => setReplying(!isReplying)}
|
||||
icon={ICONS.REPLY}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isExpanded && comments && (end < numberOfComments || start > 0) && (
|
||||
<div className="comment__actions">
|
||||
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isReplying ? (
|
||||
<CommentCreate
|
||||
isNested={isExpanded}
|
||||
key={parentId}
|
||||
uri={uri}
|
||||
parentId={parentId}
|
||||
onDoneReplying={() => handleCommentDone()}
|
||||
onCancelReplying={() => setReplying(false)}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommentsReplies;
|
|
@ -131,6 +131,17 @@ export class FormField extends React.PureComponent<Props> {
|
|||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else if (type === 'select-tiny') {
|
||||
input = (
|
||||
<fieldset-section class="select--slim">
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||
)}
|
||||
<select id={name} {...inputProps}>
|
||||
{children}
|
||||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else if (type === 'markdown') {
|
||||
const handleEvents = {
|
||||
contextmenu: openEditorMenu,
|
||||
|
|
|
@ -721,4 +721,9 @@ export const icons = {
|
|||
<line x1="9" y1="21" x2="9" y2="9" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.REPLY]: buildIcon(
|
||||
<g>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ const DEBOUNCE_SCROLL_HANDLER_MS = 300;
|
|||
type Props = {
|
||||
children: any,
|
||||
lastUpdateDate?: any,
|
||||
skipWait?: boolean,
|
||||
};
|
||||
|
||||
export default function WaitUntilOnPage(props: Props) {
|
||||
|
@ -45,5 +46,5 @@ export default function WaitUntilOnPage(props: Props) {
|
|||
}
|
||||
}, [ref, setShouldRender, shouldRender]);
|
||||
|
||||
return <div ref={ref}>{shouldRender && props.children}</div>;
|
||||
return <div ref={ref}>{(props.skipWait || shouldRender) && props.children}</div>;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ type Props = {
|
|||
label?: string,
|
||||
injected?: Array<string>,
|
||||
emailVerified: boolean,
|
||||
tiny: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -91,7 +92,7 @@ class ChannelSelection extends React.PureComponent<Props, State> {
|
|||
|
||||
render() {
|
||||
const channel = this.state.addingChannel ? CHANNEL_NEW : this.props.channel;
|
||||
const { fetchingChannels, channels = [], hideAnon, hideNew, label, injected = [] } = this.props;
|
||||
const { fetchingChannels, channels = [], hideAnon, hideNew, label, injected = [], tiny } = this.props;
|
||||
const { addingChannel } = this.state;
|
||||
|
||||
return (
|
||||
|
@ -99,8 +100,9 @@ class ChannelSelection extends React.PureComponent<Props, State> {
|
|||
<FormField
|
||||
id={ID_FF_SELECT_CHANNEL}
|
||||
name="channel"
|
||||
label={label || __('Channel')}
|
||||
type="select"
|
||||
label={!tiny && (label || __('Channel'))}
|
||||
labelOnLeft={tiny}
|
||||
type={tiny ? 'select-tiny' : 'select'}
|
||||
onChange={this.handleChannelChange}
|
||||
value={channel}
|
||||
>
|
||||
|
|
|
@ -113,3 +113,4 @@ export const OPEN_LOG_FOLDER = 'Folder';
|
|||
export const LBRY_STATUS = 'BarChart';
|
||||
export const NOTIFICATION = 'Bell';
|
||||
export const LAYOUT = 'Layout';
|
||||
export const REPLY = 'Reply';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
||||
import { doSetContentHistoryItem } from 'redux/actions/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
doFetchFileInfo,
|
||||
makeSelectFileInfoForUri,
|
||||
|
@ -13,17 +14,25 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
|
|||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import FilePage from './view';
|
||||
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
||||
|
||||
const select = (state, props) => ({
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowMatureContent(state),
|
||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const linkedCommentId = urlParams.get('lc');
|
||||
|
||||
return {
|
||||
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowMatureContent(state),
|
||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
|
@ -32,4 +41,4 @@ const perform = dispatch => ({
|
|||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePage);
|
||||
export default withRouter(connect(select, perform)(FilePage));
|
||||
|
|
|
@ -7,14 +7,12 @@ import FileTitle from 'component/fileTitle';
|
|||
import FileRenderInitiator from 'component/fileRenderInitiator';
|
||||
import FileRenderInline from 'component/fileRenderInline';
|
||||
import FileRenderDownload from 'component/fileRenderDownload';
|
||||
import Card from 'component/common/card';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import FileValues from 'component/fileValues';
|
||||
import FileDescription from 'component/fileDescription';
|
||||
import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
||||
// import WaitUntilOnPage from 'component/common/wait-until-on-page';
|
||||
import RecommendedContent from 'component/recommendedContent';
|
||||
import CommentsList from 'component/commentsList';
|
||||
import CommentCreate from 'component/commentCreate';
|
||||
|
||||
export const FILE_WRAPPER_CLASS = 'file-page__video-container';
|
||||
|
||||
|
@ -31,6 +29,7 @@ type Props = {
|
|||
markSubscriptionRead: (string, string) => void,
|
||||
obscureNsfw: boolean,
|
||||
isMature: boolean,
|
||||
linkedComment: any,
|
||||
};
|
||||
|
||||
class FilePage extends React.Component<Props> {
|
||||
|
@ -136,7 +135,7 @@ class FilePage extends React.Component<Props> {
|
|||
lastReset: ?any;
|
||||
|
||||
render() {
|
||||
const { uri, renderMode, costInfo, obscureNsfw, isMature } = this.props;
|
||||
const { uri, renderMode, costInfo, obscureNsfw, isMature, linkedComment } = this.props;
|
||||
|
||||
if (obscureNsfw && isMature) {
|
||||
return this.renderBlockedPage();
|
||||
|
@ -149,17 +148,9 @@ class FilePage extends React.Component<Props> {
|
|||
<FileDescription uri={uri} />
|
||||
<FileValues uri={uri} />
|
||||
<FileDetails uri={uri} />
|
||||
<Card
|
||||
title={__('Leave a Comment')}
|
||||
actions={
|
||||
<div>
|
||||
<CommentCreate uri={uri} />
|
||||
<WaitUntilOnPage lastUpdateDate={this.lastReset}>
|
||||
<CommentsList uri={uri} />
|
||||
</WaitUntilOnPage>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{/* <WaitUntilOnPage lastUpdateDate={this.lastReset} skipWait={Boolean(linkedComment)}> */}
|
||||
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||
{/* </WaitUntilOnPage> */}
|
||||
</div>
|
||||
|
||||
<RecommendedContent uri={uri} />
|
||||
|
|
|
@ -5,8 +5,11 @@ import { handleActions } from 'util/redux-utils';
|
|||
const defaultState: CommentsState = {
|
||||
commentById: {}, // commentId -> Comment
|
||||
byId: {}, // ClaimID -> list of comments
|
||||
repliesByParentId: {}, // ParentCommentID -> list of reply comments
|
||||
topLevelCommentsById: {}, // ClaimID -> list of top level comments
|
||||
commentsByUri: {}, // URI -> claimId
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
myComments: undefined,
|
||||
};
|
||||
|
||||
|
@ -14,18 +17,20 @@ export default handleActions(
|
|||
{
|
||||
[ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({
|
||||
...state,
|
||||
isLoading: true,
|
||||
isCommenting: true,
|
||||
}),
|
||||
|
||||
[ACTIONS.COMMENT_CREATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
}),
|
||||
|
||||
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
||||
const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data;
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||
const comments = byId[claimId] || [];
|
||||
const newCommentIds = comments.slice();
|
||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||
|
@ -37,16 +42,30 @@ export default handleActions(
|
|||
newCommentIds.unshift(comment.comment_id);
|
||||
byId[claimId] = newCommentIds;
|
||||
|
||||
if (!commentsByUri[uri]) {
|
||||
commentsByUri[uri] = claimId;
|
||||
if (comment['parent_id']) {
|
||||
if (!repliesByParentId[comment.parent_id]) {
|
||||
repliesByParentId[comment.parent_id] = [comment.comment_id];
|
||||
} else {
|
||||
repliesByParentId[comment.parent_id].unshift(comment.comment_id);
|
||||
}
|
||||
} else {
|
||||
if (!topLevelCommentsById[claimId]) {
|
||||
commentsByUri[uri] = claimId;
|
||||
topLevelCommentsById[claimId] = [comment.comment_id];
|
||||
} else {
|
||||
topLevelCommentsById[claimId].unshift(comment.comment_id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
topLevelCommentsById,
|
||||
repliesByParentId,
|
||||
commentById,
|
||||
byId,
|
||||
commentsByUri,
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -57,8 +76,11 @@ export default handleActions(
|
|||
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||
|
||||
const tempRepliesByParent = {};
|
||||
const topLevelComments = [];
|
||||
if (comments) {
|
||||
// we use an Array to preserve order of listing
|
||||
// in reality this doesn't matter and we can just
|
||||
|
@ -67,15 +89,32 @@ export default handleActions(
|
|||
|
||||
// map the comment_ids to the new comments
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
const comment = comments[i];
|
||||
if (comment['parent_id']) {
|
||||
if (!tempRepliesByParent[comment.parent_id]) {
|
||||
tempRepliesByParent[comment.parent_id] = [comment.comment_id];
|
||||
} else {
|
||||
tempRepliesByParent[comment.parent_id].push(comment.comment_id);
|
||||
}
|
||||
} else {
|
||||
commentById[comment.comment_id] = comment;
|
||||
topLevelComments.push(comment.comment_id);
|
||||
}
|
||||
commentIds[i] = comments[i].comment_id;
|
||||
commentById[commentIds[i]] = comments[i];
|
||||
}
|
||||
topLevelCommentsById[claimId] = topLevelComments;
|
||||
|
||||
byId[claimId] = commentIds;
|
||||
commentsByUri[uri] = claimId;
|
||||
}
|
||||
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId, tempRepliesByParent); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||
|
||||
return {
|
||||
...state,
|
||||
topLevelCommentsById,
|
||||
repliesByParentId,
|
||||
byId,
|
||||
commentById,
|
||||
commentsByUri,
|
||||
|
@ -116,12 +155,12 @@ export default handleActions(
|
|||
// do nothing
|
||||
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
}),
|
||||
// do nothing
|
||||
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
|
||||
...state,
|
||||
isLoading: true,
|
||||
isCommenting: true,
|
||||
}),
|
||||
// replace existing comment with comment returned here under its comment_id
|
||||
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
|
||||
|
@ -132,13 +171,13 @@ export default handleActions(
|
|||
return {
|
||||
...state,
|
||||
commentById,
|
||||
isLoading: false,
|
||||
isCommenting: false,
|
||||
};
|
||||
},
|
||||
// nothing can be done here
|
||||
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||
...state,
|
||||
isLoading: false,
|
||||
isCmmenting: false,
|
||||
}),
|
||||
// nothing can really be done here
|
||||
[ACTIONS.COMMENT_HIDE_STARTED]: (state: CommentsState, action: any) => ({
|
||||
|
|
|
@ -9,6 +9,16 @@ export const selectBlockedChannels = createSelector(selectState, (state: Blockli
|
|||
|
||||
export const selectBlockedChannelsCount = createSelector(selectBlockedChannels, (state: Array<string>) => state.length);
|
||||
|
||||
export const selectBlockedChannelsObj = createSelector(selectState, (state: BlocklistState) => {
|
||||
return state.blockedChannels.reduce((acc: any, val: any) => {
|
||||
const outpoint = `${val.txid}:${String(val.nout)}`;
|
||||
return {
|
||||
...acc,
|
||||
[outpoint]: 1,
|
||||
};
|
||||
}, {});
|
||||
});
|
||||
|
||||
export const selectChannelIsBlocked = (uri: string) =>
|
||||
createSelector(selectBlockedChannels, (state: Array<string>) => {
|
||||
return state.includes(uri);
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as SETTINGS from 'constants/settings';
|
|||
import { createSelector } from 'reselect';
|
||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
|
||||
|
||||
const selectState = state => state.comments || {};
|
||||
|
@ -12,10 +12,29 @@ export const selectCommentsById = createSelector(selectState, state => state.com
|
|||
|
||||
export const selectIsFetchingComments = createSelector(selectState, state => state.isLoading);
|
||||
|
||||
export const selectIsPostingComment = createSelector(selectState, state => state.isCommenting);
|
||||
|
||||
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||
const byClaimId = state.byId || {};
|
||||
const comments = {};
|
||||
|
||||
// replace every comment_id in the list with the actual comment object
|
||||
Object.keys(byClaimId).forEach((claimId: string) => {
|
||||
const commentIds = byClaimId[claimId];
|
||||
|
||||
comments[claimId] = Array(commentIds === null ? 0 : commentIds.length);
|
||||
for (let i = 0; i < commentIds.length; i++) {
|
||||
comments[claimId][i] = byId[commentIds[i]];
|
||||
}
|
||||
});
|
||||
|
||||
return comments;
|
||||
});
|
||||
|
||||
export const selectTopLevelCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||
const byClaimId = state.topLevelCommentsById || {};
|
||||
const comments = {};
|
||||
|
||||
// replace every comment_id in the list with the actual comment object
|
||||
Object.keys(byClaimId).forEach(claimId => {
|
||||
const commentIds = byClaimId[claimId];
|
||||
|
@ -29,6 +48,26 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
|||
return comments;
|
||||
});
|
||||
|
||||
export const makeSelectCommentForCommentId = (commentId: string) =>
|
||||
createSelector(selectCommentsById, comments => comments[commentId]);
|
||||
|
||||
export const selectRepliesByParentId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||
const byParentId = state.repliesByParentId || {};
|
||||
const comments = {};
|
||||
|
||||
// replace every comment_id in the list with the actual comment object
|
||||
Object.keys(byParentId).forEach(id => {
|
||||
const commentIds = byParentId[id];
|
||||
|
||||
comments[id] = Array(commentIds === null ? 0 : commentIds.length);
|
||||
for (let i = 0; i < commentIds.length; i++) {
|
||||
comments[id][i] = byId[commentIds[i]];
|
||||
}
|
||||
});
|
||||
|
||||
return comments;
|
||||
});
|
||||
|
||||
// previously this used a mapping from claimId -> Array<Comments>
|
||||
/* export const selectCommentsById = createSelector(
|
||||
selectState,
|
||||
|
@ -56,39 +95,112 @@ export const makeSelectCommentsForUri = (uri: string) =>
|
|||
selectClaimsById,
|
||||
selectMyActiveClaims,
|
||||
selectBlockedChannels,
|
||||
selectBlackListedOutpoints,
|
||||
selectFilteredOutpoints,
|
||||
selectBlacklistedOutpointMap,
|
||||
selectFilteredOutpointMap,
|
||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||
(
|
||||
byClaimId,
|
||||
byUri,
|
||||
claimsById,
|
||||
myClaims,
|
||||
blockedChannels,
|
||||
blacklistedOutpoints,
|
||||
filteredOutpoints,
|
||||
showMatureContent
|
||||
) => {
|
||||
(byClaimId, byUri, claimsById, myClaims, blockedChannels, blacklistedMap, filteredMap, showMatureContent) => {
|
||||
const claimId = byUri[uri];
|
||||
const comments = byClaimId && byClaimId[claimId];
|
||||
const blacklistedMap = blacklistedOutpoints
|
||||
? blacklistedOutpoints.reduce((acc, val) => {
|
||||
const outpoint = `${val.txid}:${val.nout}`;
|
||||
return {
|
||||
...acc,
|
||||
[outpoint]: 1,
|
||||
};
|
||||
}, {})
|
||||
: {};
|
||||
const filteredMap = filteredOutpoints
|
||||
? filteredOutpoints.reduce((acc, val) => {
|
||||
const outpoint = `${val.txid}:${val.nout}`;
|
||||
return {
|
||||
...acc,
|
||||
[outpoint]: 1,
|
||||
};
|
||||
}, {})
|
||||
: {};
|
||||
|
||||
return comments
|
||||
? comments.filter(comment => {
|
||||
const channelClaim = claimsById[comment.channel_id];
|
||||
|
||||
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||
if (channelClaim) {
|
||||
if (myClaims && myClaims.size > 0) {
|
||||
const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id);
|
||||
if (claimIsMine) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const outpoint = `${channelClaim.txid}:${channelClaim.nout}`;
|
||||
if (blacklistedMap[outpoint] || filteredMap[outpoint]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!showMatureContent) {
|
||||
const claimIsMature = isClaimNsfw(channelClaim);
|
||||
if (claimIsMature) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !blockedChannels.includes(comment.channel_url);
|
||||
})
|
||||
: [];
|
||||
}
|
||||
);
|
||||
|
||||
export const makeSelectTopLevelCommentsForUri = (uri: string) =>
|
||||
createSelector(
|
||||
selectTopLevelCommentsByClaimId,
|
||||
selectCommentsByUri,
|
||||
selectClaimsById,
|
||||
selectMyActiveClaims,
|
||||
selectBlockedChannels,
|
||||
selectBlacklistedOutpointMap,
|
||||
selectFilteredOutpointMap,
|
||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||
(byClaimId, byUri, claimsById, myClaims, blockedChannels, blacklistedMap, filteredMap, showMatureContent) => {
|
||||
const claimId = byUri[uri];
|
||||
const comments = byClaimId && byClaimId[claimId];
|
||||
|
||||
return comments
|
||||
? comments.filter(comment => {
|
||||
const channelClaim = claimsById[comment.channel_id];
|
||||
|
||||
// Return comment if `channelClaim` doesn't exist so the component knows to resolve the author
|
||||
if (channelClaim) {
|
||||
if (myClaims && myClaims.size > 0) {
|
||||
const claimIsMine = channelClaim.is_my_output || myClaims.has(channelClaim.claim_id);
|
||||
if (claimIsMine) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const outpoint = `${channelClaim.txid}:${channelClaim.nout}`;
|
||||
if (blacklistedMap[outpoint] || filteredMap[outpoint]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!showMatureContent) {
|
||||
const claimIsMature = isClaimNsfw(channelClaim);
|
||||
if (claimIsMature) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !blockedChannels.includes(comment.channel_url);
|
||||
})
|
||||
: [];
|
||||
}
|
||||
);
|
||||
|
||||
export const makeSelectRepliesForParentId = (id: string) =>
|
||||
createSelector(
|
||||
selectState, // no selectRepliesByParentId
|
||||
selectCommentsById,
|
||||
selectClaimsById,
|
||||
selectMyActiveClaims,
|
||||
selectBlockedChannels,
|
||||
selectBlacklistedOutpointMap,
|
||||
selectFilteredOutpointMap,
|
||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||
(state, commentsById, claimsById, myClaims, blockedChannels, blacklistedMap, filteredMap, showMatureContent) => {
|
||||
// const claimId = byUri[uri]; // just parentId (id)
|
||||
const replyIdsByParentId = state.repliesByParentId;
|
||||
const replyIdsForParent = replyIdsByParentId[id] || [];
|
||||
if (!replyIdsForParent.length) return null;
|
||||
|
||||
const comments = [];
|
||||
replyIdsForParent.forEach(cid => {
|
||||
comments.push(commentsById[cid]);
|
||||
});
|
||||
// const comments = byParentId && byParentId[id];
|
||||
|
||||
return comments
|
||||
? comments.filter(comment => {
|
||||
|
|
|
@ -1,75 +1,145 @@
|
|||
$thumbnailWidth: 3rem;
|
||||
$thumbnailWidthSmall: 2rem;
|
||||
|
||||
.comments {
|
||||
padding-top: var(--spacing-l);
|
||||
list-style-type: none;
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
.comments--replies {
|
||||
margin-left: var(--spacing-m);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.comment__create {
|
||||
padding-bottom: var(--spacing-l);
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
margin-left: calc(#{$thumbnailWidth} + var(--spacing-m));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: var(--font-body);
|
||||
font-size: var(--font-small);
|
||||
margin: 0;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding: var(--spacing-m) 0;
|
||||
}
|
||||
&:last-of-type {
|
||||
padding-top: var(--spacing-m);
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(3rem);
|
||||
@include handleChannelGif($thumbnailWidthSmall);
|
||||
margin-right: 0;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
@include handleChannelGif($thumbnailWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
margin-top: var(--spacing-s);
|
||||
.comment__replies-container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comment__replies {
|
||||
display: flex;
|
||||
margin-top: var(--spacing-m);
|
||||
margin-left: calc(#{$thumbnailWidthSmall} + var(--spacing-xs));
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc(#{$thumbnailWidth} + var(--spacing-m));
|
||||
}
|
||||
}
|
||||
|
||||
.comment__reply {
|
||||
border-left: 5px solid var(--color-primary-alt);
|
||||
margin-left: var(--spacing-m);
|
||||
margin: 0;
|
||||
|
||||
.comment__author-thumbnail {
|
||||
margin-left: var(--spacing-m);
|
||||
&:not(:first-child) {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__reply-button {
|
||||
margin-top: var(--spacing-s);
|
||||
.comment__threadline {
|
||||
@extend .button--alt;
|
||||
height: auto;
|
||||
align-self: stretch;
|
||||
padding: 2px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--color-comment-threadline);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--color-comment-threadline-hover);
|
||||
background-color: var(--color-comment-threadline-hover);
|
||||
border-color: var(--color-comment-threadline-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.comment-new__label-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-new__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment__highlighted {
|
||||
background: var(--color-comment-highlighted);
|
||||
box-shadow: 0 0 0 var(--spacing-xs) var(--color-comment-highlighted);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.comment__body_container {
|
||||
padding-right: var(--spacing-s);
|
||||
flex: 1;
|
||||
width: 80%;
|
||||
margin-left: var(--spacing-xs);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__meta-information {
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.comment__message {
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__author {
|
||||
text-overflow: ellipsis;
|
||||
padding-right: var(--spacing-xs);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.comment__time {
|
||||
opacity: 0.3;
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.comment__menu {
|
||||
|
@ -109,3 +179,49 @@
|
|||
border-radius: var(--card-radius);
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
|
||||
.comment__actions {
|
||||
display: flex;
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc(#{$thumbnailWidthSmall} + var(--spacing-xs));
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.button__label {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc(#{$thumbnailWidth} + var(--spacing-m));
|
||||
}
|
||||
}
|
||||
|
||||
.comment__action {
|
||||
@extend .button--uri-indicator;
|
||||
height: auto;
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.comment__action--nested {
|
||||
@extend .comment__action;
|
||||
}
|
||||
|
||||
.comment__action--nested,
|
||||
.comment__create--nested-reply {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc((#{$thumbnailWidth} + var(--spacing-m)) * 2 + var(--spacing-m) + 4px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__more-below {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
|
|
@ -413,7 +413,9 @@ fieldset-group {
|
|||
}
|
||||
|
||||
.form-field__two-column {
|
||||
column-count: 2;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
|
@ -431,4 +433,11 @@ fieldset-section {
|
|||
margin-top: var(--spacing-s); // Extra specificity needed here since _section.scss is applied after this file
|
||||
}
|
||||
}
|
||||
|
||||
.select--slim {
|
||||
select {
|
||||
max-height: 1.5rem !important;
|
||||
padding: 0 var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,8 +45,6 @@
|
|||
font-size: 1em;
|
||||
}
|
||||
|
||||
p,
|
||||
blockquote,
|
||||
dl,
|
||||
ul,
|
||||
ol,
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
--color-input-color: #111111;
|
||||
--color-input-label: var(--color-gray-5);
|
||||
--color-input-placeholder: #212529;
|
||||
--color-input-bg: var(--color-gray-1);
|
||||
--color-input-bg: #f4f4f4;
|
||||
--color-input-bg-copyable: #434b53;
|
||||
--color-input-border: var(--color-border);
|
||||
--color-input-border-active: var(--color-secondary);
|
||||
|
|
|
@ -44,12 +44,6 @@ h6 {
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
& + p {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
li {
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
--color-purchased: #ffd580;
|
||||
--color-purchased-alt: var(--color-purchased);
|
||||
--color-purchased-text: var(--color-gray-5);
|
||||
--color-comment-highlighted: #484734;
|
||||
|
||||
// Text
|
||||
--color-text: #eeeeee;
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
--color-purchased: var(--color-cost);
|
||||
--color-purchased-alt: #ffebc2;
|
||||
--color-purchased-text: var(--color-gray-5);
|
||||
--color-comment-highlighted: #fff2d9;
|
||||
--color-comment-threadline: var(--color-gray-2);
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
--color-comment-threadline-border: var(--color-gray-2);
|
||||
|
||||
// Icons
|
||||
--color-follow-bg: #ffd4da;
|
||||
|
|
Loading…
Reference in a new issue