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 { doToggleBlockChannel } from 'redux/actions/blocked';
|
||||||
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
|
||||||
import Comment from './view';
|
import Comment from './view';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import { selectIsFetchingComments } from 'redux/selectors/comments';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
pending: props.authorUri && makeSelectClaimIsPending(props.authorUri)(state),
|
pending: props.authorUri && makeSelectClaimIsPending(props.authorUri)(state),
|
||||||
|
@ -17,6 +19,8 @@ const select = (state, props) => ({
|
||||||
isResolvingUri: props.authorUri && makeSelectIsUriResolving(props.authorUri)(state),
|
isResolvingUri: props.authorUri && makeSelectIsUriResolving(props.authorUri)(state),
|
||||||
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
thumbnail: props.authorUri && makeSelectThumbnailForUri(props.authorUri)(state),
|
||||||
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
|
channelIsBlocked: props.authorUri && selectChannelIsBlocked(props.authorUri)(state),
|
||||||
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
|
|
@ -6,13 +6,12 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { isEmpty } from 'util/object';
|
import { isEmpty } from 'util/object';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Expandable from 'component/expandable';
|
// import Expandable from 'component/expandable';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview';
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { FormField, Form } from 'component/common/form';
|
import { FormField, Form } from 'component/common/form';
|
||||||
import CommentCreate from 'component/commentCreate';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
|
||||||
|
@ -34,6 +33,7 @@ type Props = {
|
||||||
updateComment: (string, string) => void,
|
updateComment: (string, string) => void,
|
||||||
deleteComment: string => void,
|
deleteComment: string => void,
|
||||||
blockChannel: string => void,
|
blockChannel: string => void,
|
||||||
|
linkedComment?: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
@ -57,6 +57,7 @@ function Comment(props: Props) {
|
||||||
updateComment,
|
updateComment,
|
||||||
deleteComment,
|
deleteComment,
|
||||||
blockChannel,
|
blockChannel,
|
||||||
|
linkedComment,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isEditing, setEditing] = useState(false);
|
const [isEditing, setEditing] = useState(false);
|
||||||
|
@ -65,11 +66,7 @@ function Comment(props: Props) {
|
||||||
|
|
||||||
// used for controlling the visibility of the menu icon
|
// used for controlling the visibility of the menu icon
|
||||||
const [mouseIsHovering, setMouseHover] = useState(false);
|
const [mouseIsHovering, setMouseHover] = useState(false);
|
||||||
|
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
// used for controlling visibility of reply comment component
|
|
||||||
const [isReplying, setReplying] = useState(false);
|
|
||||||
|
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
|
||||||
|
|
||||||
// to debounce subsequent requests
|
// to debounce subsequent requests
|
||||||
const shouldFetch =
|
const shouldFetch =
|
||||||
|
@ -108,12 +105,15 @@ function Comment(props: Props) {
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
updateComment(commentId, editedMessage);
|
updateComment(commentId, editedMessage);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setReplying(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<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)}
|
onMouseOver={() => setMouseHover(true)}
|
||||||
onMouseOut={() => setMouseHover(false)}
|
onMouseOut={() => setMouseHover(false)}
|
||||||
>
|
>
|
||||||
|
@ -133,9 +133,16 @@ function Comment(props: Props) {
|
||||||
label={author}
|
label={author}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<time className="comment__time" dateTime={timePosted}>
|
{/* // link here */}
|
||||||
{DateTime.getTimeAgoStr(timePosted)}
|
<Button
|
||||||
</time>
|
navigate={`${uri}?lc=${commentId}`}
|
||||||
|
label={
|
||||||
|
<time className="comment__time" dateTime={timePosted}>
|
||||||
|
{DateTime.getTimeAgoStr(timePosted)}
|
||||||
|
</time>
|
||||||
|
}
|
||||||
|
className="button--uri-indicator"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="comment__menu">
|
<div className="comment__menu">
|
||||||
<Menu>
|
<Menu>
|
||||||
|
@ -174,8 +181,6 @@ function Comment(props: Props) {
|
||||||
value={editedMessage}
|
value={editedMessage}
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
onChange={handleEditMessageChanged}
|
onChange={handleEditMessageChanged}
|
||||||
quickActionLabel={!SIMPLE_SITE && (advancedEditor ? __('Simple Editor') : __('Advanced Editor'))}
|
|
||||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
|
@ -191,9 +196,9 @@ function Comment(props: Props) {
|
||||||
</Form>
|
</Form>
|
||||||
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
) : editedMessage.length >= LENGTH_TO_COLLAPSE ? (
|
||||||
<div className="comment__message">
|
<div className="comment__message">
|
||||||
<Expandable>
|
{/* <Expandable> */}
|
||||||
<MarkdownPreview content={message} />
|
<MarkdownPreview content={message} />
|
||||||
</Expandable>
|
{/* </Expandable> */}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="comment__message">
|
<div className="comment__message">
|
||||||
|
@ -201,27 +206,6 @@ function Comment(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Props = {
|
||||||
parentId?: string,
|
parentId?: string,
|
||||||
onDoneReplying?: () => void,
|
onDoneReplying?: () => void,
|
||||||
onCancelReplying?: () => void,
|
onCancelReplying?: () => void,
|
||||||
|
isNested: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommentCreate(props: Props) {
|
export function CommentCreate(props: Props) {
|
||||||
|
@ -33,6 +34,7 @@ export function CommentCreate(props: Props) {
|
||||||
parentId,
|
parentId,
|
||||||
onDoneReplying,
|
onDoneReplying,
|
||||||
onCancelReplying,
|
onCancelReplying,
|
||||||
|
isNested,
|
||||||
} = props;
|
} = props;
|
||||||
const { claim_id: claimId } = claim;
|
const { claim_id: claimId } = claim;
|
||||||
const isReply = !!parentId;
|
const isReply = !!parentId;
|
||||||
|
@ -53,9 +55,9 @@ export function CommentCreate(props: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// set default channel
|
// set default channel
|
||||||
if ((channel === '' || channel === 'anonymous') && topChannel) {
|
if ((channel === '' || channel === 'anonymous') && topChannel) {
|
||||||
handleChannelChange(topChannel.name);
|
setChannel(topChannel.name);
|
||||||
}
|
}
|
||||||
}, [channel, topChannel]);
|
}, [channel, topChannel, setChannel]);
|
||||||
|
|
||||||
function handleCommentChange(event) {
|
function handleCommentChange(event) {
|
||||||
let commentValue;
|
let commentValue;
|
||||||
|
@ -68,10 +70,6 @@ export function CommentCreate(props: Props) {
|
||||||
setCommentValue(commentValue);
|
setCommentValue(commentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChannelChange(channel) {
|
|
||||||
setChannel(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommentAck() {
|
function handleCommentAck() {
|
||||||
setCommentAck(true);
|
setCommentAck(true);
|
||||||
}
|
}
|
||||||
|
@ -107,17 +105,27 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} className={classnames('comment__create', { 'comment__create--reply': isReply })}>
|
<Form
|
||||||
{!isReply && <ChannelSelection channel={channel} hideAnon onChannelChange={handleChannelChange} />}
|
onSubmit={handleSubmit}
|
||||||
|
className={classnames('comment__create', {
|
||||||
|
'comment__create--reply': isReply,
|
||||||
|
'comment__create--nested-reply': isNested,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
disabled={channel === CHANNEL_NEW}
|
disabled={channel === CHANNEL_NEW}
|
||||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||||
name={isReply ? 'content_reply' : 'content_description'}
|
name={isReply ? 'content_reply' : '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={
|
quickActionLabel={
|
||||||
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
!SIMPLE_SITE && (isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor'))
|
||||||
}
|
}
|
||||||
quickActionHandler={!SIMPLE_SITE && (isReply ? undefined : toggleEditorMode)}
|
quickActionHandler={!SIMPLE_SITE && toggleEditorMode}
|
||||||
onFocus={onTextareaFocus}
|
onFocus={onTextareaFocus}
|
||||||
placeholder={__('Say something about this...')}
|
placeholder={__('Say something about this...')}
|
||||||
value={commentValue}
|
value={commentValue}
|
||||||
|
@ -126,7 +134,7 @@ export function CommentCreate(props: Props) {
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
<div className="section__actions">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
disabled={channel === CHANNEL_NEW || !commentValue.length}
|
disabled={channel === CHANNEL_NEW || !commentValue.length}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-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 { doCommentList } from 'redux/actions/comments';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
comments: makeSelectCommentsForUri(props.uri)(state),
|
comments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
isFetchingComments: selectIsFetchingComments(state),
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Comment from 'component/comment';
|
import Comment from 'component/comment';
|
||||||
import Spinner from 'component/spinner';
|
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 = {
|
type Props = {
|
||||||
comments: Array<any>,
|
comments: Array<any>,
|
||||||
|
@ -10,11 +14,28 @@ type Props = {
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
isFetchingComments: boolean,
|
isFetchingComments: boolean,
|
||||||
|
linkedComment: any,
|
||||||
|
commentingEnabled: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentList(props: Props) {
|
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
|
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||||
const isMyComment = (channelId: string) => {
|
const isMyComment = (channelId: string) => {
|
||||||
if (myChannels != null && channelId != null) {
|
if (myChannels != null && channelId != null) {
|
||||||
|
@ -27,56 +48,95 @@ function CommentList(props: Props) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMoreBelow = () => {
|
||||||
|
if (moreBelow) {
|
||||||
|
setEnd(end + 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commentRef = React.useRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchComments(uri);
|
fetchComments(uri);
|
||||||
}, [fetchComments, uri]);
|
}, [fetchComments, uri]);
|
||||||
|
|
||||||
function sortByParent(arrayOfComments) {
|
useEffect(() => {
|
||||||
let parentComments = arrayOfComments.filter(comment => comment.parent_id === undefined);
|
if (linkedCommentId && commentRef && commentRef.current) {
|
||||||
let childComments = arrayOfComments.filter(comment => comment.parent_id !== undefined);
|
commentRef.current.scrollIntoView({ block: 'start' });
|
||||||
let sortedArray = [];
|
window.scrollBy(0, -100);
|
||||||
|
}
|
||||||
|
}, [linkedCommentId]);
|
||||||
|
|
||||||
parentComments.forEach(parentComment => {
|
function prepareComments(arrayOfComments, linkedComment) {
|
||||||
sortedArray.push(parentComment);
|
let orderedComments = [];
|
||||||
|
|
||||||
childComments
|
if (linkedComment) {
|
||||||
.reverse()
|
if (!linkedComment.parent_id) {
|
||||||
.filter(childComment => childComment.parent_id === parentComment.comment_id)
|
orderedComments = arrayOfComments.filter(c => c.comment_id !== linkedComment.comment_id);
|
||||||
.forEach(childComment => {
|
orderedComments.unshift(linkedComment);
|
||||||
sortedArray.push(childComment);
|
} 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);
|
||||||
return sortedArray;
|
}
|
||||||
|
} else {
|
||||||
|
orderedComments = arrayOfComments;
|
||||||
|
}
|
||||||
|
return orderedComments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayedComments = prepareComments(comments, linkedComment).slice(start, end);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="comments">
|
<Card
|
||||||
{isFetchingComments && (
|
title={<span>{__('Comments')}</span>}
|
||||||
<div className="main--empty">
|
actions={
|
||||||
<Spinner />
|
<>
|
||||||
</div>
|
{commentingEnabled && <CommentCreate uri={uri} />}
|
||||||
)}
|
<ul className="comments" ref={commentRef}>
|
||||||
{!isFetchingComments &&
|
{isFetchingComments && (
|
||||||
comments &&
|
<div className="main--empty">
|
||||||
sortByParent(comments).map(comment => {
|
<Spinner />
|
||||||
return (
|
</div>
|
||||||
<Comment
|
)}
|
||||||
uri={uri}
|
{!isFetchingComments &&
|
||||||
authorUri={comment.channel_url}
|
comments &&
|
||||||
author={comment.channel_name}
|
displayedComments &&
|
||||||
claimId={comment.claim_id}
|
displayedComments.map(comment => {
|
||||||
commentId={comment.comment_id}
|
return (
|
||||||
key={comment.channel_id + comment.comment_id}
|
<>
|
||||||
message={comment.comment}
|
<Comment
|
||||||
parentId={comment.parent_id || null}
|
uri={uri}
|
||||||
timePosted={comment.timestamp * 1000}
|
authorUri={comment.channel_url}
|
||||||
claimIsMine={claimIsMine}
|
author={comment.channel_name}
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
claimId={comment.claim_id}
|
||||||
/>
|
commentId={comment.comment_id}
|
||||||
);
|
key={comment.comment_id}
|
||||||
})}
|
message={comment.comment}
|
||||||
</ul>
|
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>
|
</select>
|
||||||
</fieldset-section>
|
</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') {
|
} else if (type === 'markdown') {
|
||||||
const handleEvents = {
|
const handleEvents = {
|
||||||
contextmenu: openEditorMenu,
|
contextmenu: openEditorMenu,
|
||||||
|
|
|
@ -721,4 +721,9 @@ export const icons = {
|
||||||
<line x1="9" y1="21" x2="9" y2="9" />
|
<line x1="9" y1="21" x2="9" y2="9" />
|
||||||
</g>
|
</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 = {
|
type Props = {
|
||||||
children: any,
|
children: any,
|
||||||
lastUpdateDate?: any,
|
lastUpdateDate?: any,
|
||||||
|
skipWait?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WaitUntilOnPage(props: Props) {
|
export default function WaitUntilOnPage(props: Props) {
|
||||||
|
@ -45,5 +46,5 @@ export default function WaitUntilOnPage(props: Props) {
|
||||||
}
|
}
|
||||||
}, [ref, setShouldRender, shouldRender]);
|
}, [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,
|
label?: string,
|
||||||
injected?: Array<string>,
|
injected?: Array<string>,
|
||||||
emailVerified: boolean,
|
emailVerified: boolean,
|
||||||
|
tiny: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -91,7 +92,7 @@ class ChannelSelection extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const channel = this.state.addingChannel ? CHANNEL_NEW : this.props.channel;
|
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;
|
const { addingChannel } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -99,8 +100,9 @@ class ChannelSelection extends React.PureComponent<Props, State> {
|
||||||
<FormField
|
<FormField
|
||||||
id={ID_FF_SELECT_CHANNEL}
|
id={ID_FF_SELECT_CHANNEL}
|
||||||
name="channel"
|
name="channel"
|
||||||
label={label || __('Channel')}
|
label={!tiny && (label || __('Channel'))}
|
||||||
type="select"
|
labelOnLeft={tiny}
|
||||||
|
type={tiny ? 'select-tiny' : 'select'}
|
||||||
onChange={this.handleChannelChange}
|
onChange={this.handleChannelChange}
|
||||||
value={channel}
|
value={channel}
|
||||||
>
|
>
|
||||||
|
|
|
@ -113,3 +113,4 @@ export const OPEN_LOG_FOLDER = 'Folder';
|
||||||
export const LBRY_STATUS = 'BarChart';
|
export const LBRY_STATUS = 'BarChart';
|
||||||
export const NOTIFICATION = 'Bell';
|
export const NOTIFICATION = 'Bell';
|
||||||
export const LAYOUT = 'Layout';
|
export const LAYOUT = 'Layout';
|
||||||
|
export const REPLY = 'Reply';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
||||||
import { doSetContentHistoryItem } from 'redux/actions/content';
|
import { doSetContentHistoryItem } from 'redux/actions/content';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
import {
|
import {
|
||||||
doFetchFileInfo,
|
doFetchFileInfo,
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
|
@ -13,17 +14,25 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
import FilePage from './view';
|
import FilePage from './view';
|
||||||
|
import { makeSelectCommentForCommentId } from 'redux/selectors/comments';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
const { search } = props.location;
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
const urlParams = new URLSearchParams(search);
|
||||||
obscureNsfw: !selectShowMatureContent(state),
|
const linkedCommentId = urlParams.get('lc');
|
||||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
return {
|
||||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
linkedComment: makeSelectCommentForCommentId(linkedCommentId)(state),
|
||||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(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 => ({
|
const perform = dispatch => ({
|
||||||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||||
|
@ -32,4 +41,4 @@ const perform = dispatch => ({
|
||||||
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
|
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 FileRenderInitiator from 'component/fileRenderInitiator';
|
||||||
import FileRenderInline from 'component/fileRenderInline';
|
import FileRenderInline from 'component/fileRenderInline';
|
||||||
import FileRenderDownload from 'component/fileRenderDownload';
|
import FileRenderDownload from 'component/fileRenderDownload';
|
||||||
import Card from 'component/common/card';
|
|
||||||
import FileDetails from 'component/fileDetails';
|
import FileDetails from 'component/fileDetails';
|
||||||
import FileValues from 'component/fileValues';
|
import FileValues from 'component/fileValues';
|
||||||
import FileDescription from 'component/fileDescription';
|
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 RecommendedContent from 'component/recommendedContent';
|
||||||
import CommentsList from 'component/commentsList';
|
import CommentsList from 'component/commentsList';
|
||||||
import CommentCreate from 'component/commentCreate';
|
|
||||||
|
|
||||||
export const FILE_WRAPPER_CLASS = 'file-page__video-container';
|
export const FILE_WRAPPER_CLASS = 'file-page__video-container';
|
||||||
|
|
||||||
|
@ -31,6 +29,7 @@ type Props = {
|
||||||
markSubscriptionRead: (string, string) => void,
|
markSubscriptionRead: (string, string) => void,
|
||||||
obscureNsfw: boolean,
|
obscureNsfw: boolean,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
|
linkedComment: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilePage extends React.Component<Props> {
|
class FilePage extends React.Component<Props> {
|
||||||
|
@ -136,7 +135,7 @@ class FilePage extends React.Component<Props> {
|
||||||
lastReset: ?any;
|
lastReset: ?any;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { uri, renderMode, costInfo, obscureNsfw, isMature } = this.props;
|
const { uri, renderMode, costInfo, obscureNsfw, isMature, linkedComment } = this.props;
|
||||||
|
|
||||||
if (obscureNsfw && isMature) {
|
if (obscureNsfw && isMature) {
|
||||||
return this.renderBlockedPage();
|
return this.renderBlockedPage();
|
||||||
|
@ -149,17 +148,9 @@ class FilePage extends React.Component<Props> {
|
||||||
<FileDescription uri={uri} />
|
<FileDescription uri={uri} />
|
||||||
<FileValues uri={uri} />
|
<FileValues uri={uri} />
|
||||||
<FileDetails uri={uri} />
|
<FileDetails uri={uri} />
|
||||||
<Card
|
{/* <WaitUntilOnPage lastUpdateDate={this.lastReset} skipWait={Boolean(linkedComment)}> */}
|
||||||
title={__('Leave a Comment')}
|
<CommentsList uri={uri} linkedComment={linkedComment} />
|
||||||
actions={
|
{/* </WaitUntilOnPage> */}
|
||||||
<div>
|
|
||||||
<CommentCreate uri={uri} />
|
|
||||||
<WaitUntilOnPage lastUpdateDate={this.lastReset}>
|
|
||||||
<CommentsList uri={uri} />
|
|
||||||
</WaitUntilOnPage>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RecommendedContent uri={uri} />
|
<RecommendedContent uri={uri} />
|
||||||
|
|
|
@ -5,8 +5,11 @@ import { handleActions } from 'util/redux-utils';
|
||||||
const defaultState: CommentsState = {
|
const defaultState: CommentsState = {
|
||||||
commentById: {}, // commentId -> Comment
|
commentById: {}, // commentId -> Comment
|
||||||
byId: {}, // ClaimID -> list of comments
|
byId: {}, // ClaimID -> list of comments
|
||||||
|
repliesByParentId: {}, // ParentCommentID -> list of reply comments
|
||||||
|
topLevelCommentsById: {}, // ClaimID -> list of top level comments
|
||||||
commentsByUri: {}, // URI -> claimId
|
commentsByUri: {}, // URI -> claimId
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isCommenting: false,
|
||||||
myComments: undefined,
|
myComments: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,18 +17,20 @@ export default handleActions(
|
||||||
{
|
{
|
||||||
[ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({
|
[ACTIONS.COMMENT_CREATE_STARTED]: (state: CommentsState, action: any): CommentsState => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: true,
|
isCommenting: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[ACTIONS.COMMENT_CREATE_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_CREATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
isCommenting: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
[ACTIONS.COMMENT_CREATE_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
||||||
const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data;
|
const { comment, claimId, uri }: { comment: Comment, claimId: string, uri: string } = action.data;
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
const byId = Object.assign({}, state.byId);
|
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 comments = byId[claimId] || [];
|
||||||
const newCommentIds = comments.slice();
|
const newCommentIds = comments.slice();
|
||||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||||
|
@ -37,16 +42,30 @@ export default handleActions(
|
||||||
newCommentIds.unshift(comment.comment_id);
|
newCommentIds.unshift(comment.comment_id);
|
||||||
byId[claimId] = newCommentIds;
|
byId[claimId] = newCommentIds;
|
||||||
|
|
||||||
if (!commentsByUri[uri]) {
|
if (comment['parent_id']) {
|
||||||
commentsByUri[uri] = claimId;
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
topLevelCommentsById,
|
||||||
|
repliesByParentId,
|
||||||
commentById,
|
commentById,
|
||||||
byId,
|
byId,
|
||||||
commentsByUri,
|
commentsByUri,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isCommenting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -57,8 +76,11 @@ export default handleActions(
|
||||||
|
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
const byId = Object.assign({}, state.byId);
|
const byId = Object.assign({}, state.byId);
|
||||||
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); // was byId {ClaimId -> [commentIds...]}
|
||||||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||||
|
|
||||||
|
const tempRepliesByParent = {};
|
||||||
|
const topLevelComments = [];
|
||||||
if (comments) {
|
if (comments) {
|
||||||
// we use an Array to preserve order of listing
|
// we use an Array to preserve order of listing
|
||||||
// in reality this doesn't matter and we can just
|
// 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
|
// map the comment_ids to the new comments
|
||||||
for (let i = 0; i < comments.length; i++) {
|
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;
|
commentIds[i] = comments[i].comment_id;
|
||||||
commentById[commentIds[i]] = comments[i];
|
commentById[commentIds[i]] = comments[i];
|
||||||
}
|
}
|
||||||
|
topLevelCommentsById[claimId] = topLevelComments;
|
||||||
|
|
||||||
byId[claimId] = commentIds;
|
byId[claimId] = commentIds;
|
||||||
commentsByUri[uri] = claimId;
|
commentsByUri[uri] = claimId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const repliesByParentId = Object.assign({}, state.repliesByParentId, tempRepliesByParent); // {ParentCommentID -> [commentIds...] } list of reply comments
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
topLevelCommentsById,
|
||||||
|
repliesByParentId,
|
||||||
byId,
|
byId,
|
||||||
commentById,
|
commentById,
|
||||||
commentsByUri,
|
commentsByUri,
|
||||||
|
@ -116,12 +155,12 @@ export default handleActions(
|
||||||
// do nothing
|
// do nothing
|
||||||
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_ABANDON_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
isCommenting: false,
|
||||||
}),
|
}),
|
||||||
// do nothing
|
// do nothing
|
||||||
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_UPDATE_STARTED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: true,
|
isCommenting: true,
|
||||||
}),
|
}),
|
||||||
// replace existing comment with comment returned here under its comment_id
|
// replace existing comment with comment returned here under its comment_id
|
||||||
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
|
[ACTIONS.COMMENT_UPDATE_COMPLETED]: (state: CommentsState, action: any) => {
|
||||||
|
@ -132,13 +171,13 @@ export default handleActions(
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
commentById,
|
commentById,
|
||||||
isLoading: false,
|
isCommenting: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// nothing can be done here
|
// nothing can be done here
|
||||||
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
[ACTIONS.COMMENT_UPDATE_FAILED]: (state: CommentsState, action: any) => ({
|
||||||
...state,
|
...state,
|
||||||
isLoading: false,
|
isCmmenting: false,
|
||||||
}),
|
}),
|
||||||
// nothing can really be done here
|
// nothing can really be done here
|
||||||
[ACTIONS.COMMENT_HIDE_STARTED]: (state: CommentsState, action: any) => ({
|
[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 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) =>
|
export const selectChannelIsBlocked = (uri: string) =>
|
||||||
createSelector(selectBlockedChannels, (state: Array<string>) => {
|
createSelector(selectBlockedChannels, (state: Array<string>) => {
|
||||||
return state.includes(uri);
|
return state.includes(uri);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as SETTINGS from 'constants/settings';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
import { selectBlockedChannels } from 'redux/selectors/blocked';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||||
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
|
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
|
||||||
|
|
||||||
const selectState = state => state.comments || {};
|
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 selectIsFetchingComments = createSelector(selectState, state => state.isLoading);
|
||||||
|
|
||||||
|
export const selectIsPostingComment = createSelector(selectState, state => state.isCommenting);
|
||||||
|
|
||||||
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
export const selectCommentsByClaimId = createSelector(selectState, selectCommentsById, (state, byId) => {
|
||||||
const byClaimId = state.byId || {};
|
const byClaimId = state.byId || {};
|
||||||
const comments = {};
|
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
|
// replace every comment_id in the list with the actual comment object
|
||||||
Object.keys(byClaimId).forEach(claimId => {
|
Object.keys(byClaimId).forEach(claimId => {
|
||||||
const commentIds = byClaimId[claimId];
|
const commentIds = byClaimId[claimId];
|
||||||
|
@ -29,6 +48,26 @@ export const selectCommentsByClaimId = createSelector(selectState, selectComment
|
||||||
return comments;
|
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>
|
// previously this used a mapping from claimId -> Array<Comments>
|
||||||
/* export const selectCommentsById = createSelector(
|
/* export const selectCommentsById = createSelector(
|
||||||
selectState,
|
selectState,
|
||||||
|
@ -56,39 +95,112 @@ export const makeSelectCommentsForUri = (uri: string) =>
|
||||||
selectClaimsById,
|
selectClaimsById,
|
||||||
selectMyActiveClaims,
|
selectMyActiveClaims,
|
||||||
selectBlockedChannels,
|
selectBlockedChannels,
|
||||||
selectBlackListedOutpoints,
|
selectBlacklistedOutpointMap,
|
||||||
selectFilteredOutpoints,
|
selectFilteredOutpointMap,
|
||||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||||
(
|
(byClaimId, byUri, claimsById, myClaims, blockedChannels, blacklistedMap, filteredMap, showMatureContent) => {
|
||||||
byClaimId,
|
|
||||||
byUri,
|
|
||||||
claimsById,
|
|
||||||
myClaims,
|
|
||||||
blockedChannels,
|
|
||||||
blacklistedOutpoints,
|
|
||||||
filteredOutpoints,
|
|
||||||
showMatureContent
|
|
||||||
) => {
|
|
||||||
const claimId = byUri[uri];
|
const claimId = byUri[uri];
|
||||||
const comments = byClaimId && byClaimId[claimId];
|
const comments = byClaimId && byClaimId[claimId];
|
||||||
const blacklistedMap = blacklistedOutpoints
|
|
||||||
? blacklistedOutpoints.reduce((acc, val) => {
|
return comments
|
||||||
const outpoint = `${val.txid}:${val.nout}`;
|
? comments.filter(comment => {
|
||||||
return {
|
const channelClaim = claimsById[comment.channel_id];
|
||||||
...acc,
|
|
||||||
[outpoint]: 1,
|
// 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);
|
||||||
const filteredMap = filteredOutpoints
|
if (claimIsMine) {
|
||||||
? filteredOutpoints.reduce((acc, val) => {
|
return true;
|
||||||
const outpoint = `${val.txid}:${val.nout}`;
|
}
|
||||||
return {
|
}
|
||||||
...acc,
|
|
||||||
[outpoint]: 1,
|
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
|
return comments
|
||||||
? comments.filter(comment => {
|
? comments.filter(comment => {
|
||||||
|
|
|
@ -1,75 +1,145 @@
|
||||||
|
$thumbnailWidth: 3rem;
|
||||||
|
$thumbnailWidthSmall: 2rem;
|
||||||
|
|
||||||
.comments {
|
.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 {
|
.comment {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-small);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:first-child) {
|
||||||
border-bottom: 1px solid var(--color-border);
|
margin-top: var(--spacing-l);
|
||||||
padding: var(--spacing-m) 0;
|
|
||||||
}
|
|
||||||
&:last-of-type {
|
|
||||||
padding-top: var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-thumbnail {
|
.channel-thumbnail {
|
||||||
@include handleChannelGif(3rem);
|
@include handleChannelGif($thumbnailWidthSmall);
|
||||||
|
margin-right: 0;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
@include handleChannelGif($thumbnailWidth);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__create--reply {
|
.comment__replies-container {
|
||||||
margin-top: var(--spacing-s);
|
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 {
|
.comment__reply {
|
||||||
border-left: 5px solid var(--color-primary-alt);
|
margin: 0;
|
||||||
margin-left: var(--spacing-m);
|
|
||||||
|
|
||||||
.comment__author-thumbnail {
|
&:not(:first-child) {
|
||||||
margin-left: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__reply-button {
|
.comment__threadline {
|
||||||
margin-top: var(--spacing-s);
|
@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 {
|
.comment__body_container {
|
||||||
padding-right: var(--spacing-s);
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 80%;
|
margin-left: var(--spacing-xs);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__meta {
|
.comment__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin-bottom: var(--spacing-s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__meta-information {
|
.comment__meta-information {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__message {
|
.comment__message {
|
||||||
white-space: pre-line;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__author {
|
.comment__author {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding-right: var(--spacing-xs);
|
padding-right: var(--spacing-xs);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__time {
|
.comment__time {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__menu {
|
.comment__menu {
|
||||||
|
@ -109,3 +179,49 @@
|
||||||
border-radius: var(--card-radius);
|
border-radius: var(--card-radius);
|
||||||
padding: var(--spacing-s);
|
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 {
|
.form-field__two-column {
|
||||||
column-count: 2;
|
@media (min-width: $breakpoint-small) {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__quick-action {
|
.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
|
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;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
p,
|
|
||||||
blockquote,
|
|
||||||
dl,
|
dl,
|
||||||
ul,
|
ul,
|
||||||
ol,
|
ol,
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
--color-input-color: #111111;
|
--color-input-color: #111111;
|
||||||
--color-input-label: var(--color-gray-5);
|
--color-input-label: var(--color-gray-5);
|
||||||
--color-input-placeholder: #212529;
|
--color-input-placeholder: #212529;
|
||||||
--color-input-bg: var(--color-gray-1);
|
--color-input-bg: #f4f4f4;
|
||||||
--color-input-bg-copyable: #434b53;
|
--color-input-bg-copyable: #434b53;
|
||||||
--color-input-border: var(--color-border);
|
--color-input-border: var(--color-border);
|
||||||
--color-input-border-active: var(--color-secondary);
|
--color-input-border-active: var(--color-secondary);
|
||||||
|
|
|
@ -44,12 +44,6 @@ h6 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
& + p {
|
|
||||||
margin-top: var(--spacing-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
li {
|
li {
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
--color-purchased: #ffd580;
|
--color-purchased: #ffd580;
|
||||||
--color-purchased-alt: var(--color-purchased);
|
--color-purchased-alt: var(--color-purchased);
|
||||||
--color-purchased-text: var(--color-gray-5);
|
--color-purchased-text: var(--color-gray-5);
|
||||||
|
--color-comment-highlighted: #484734;
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
--color-text: #eeeeee;
|
--color-text: #eeeeee;
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
--color-purchased: var(--color-cost);
|
--color-purchased: var(--color-cost);
|
||||||
--color-purchased-alt: #ffebc2;
|
--color-purchased-alt: #ffebc2;
|
||||||
--color-purchased-text: var(--color-gray-5);
|
--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
|
// Icons
|
||||||
--color-follow-bg: #ffd4da;
|
--color-follow-bg: #ffd4da;
|
||||||
|
|
Loading…
Add table
Reference in a new issue