mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-08-23 17:47:24 +00:00
fix comment editing
This commit is contained in:
parent
154b20c6c8
commit
f6961f91fe
11 changed files with 433 additions and 306 deletions
|
@ -1,14 +1,16 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
selectTotalStakedAmountForChannelUri,
|
selectStakedLevelForChannelUri,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
selectThumbnailForUri,
|
selectThumbnailForUri,
|
||||||
selectHasChannels,
|
selectHasChannels,
|
||||||
|
selectMyClaimIdsRaw,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doClearPlayingUri } from 'redux/actions/content';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import {
|
import {
|
||||||
selectLinkedCommentAncestors,
|
selectLinkedCommentAncestors,
|
||||||
selectOthersReactsForComment,
|
selectOthersReactsForComment,
|
||||||
|
@ -19,31 +21,34 @@ import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
import Comment from './view';
|
import Comment from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
const { comment, uri } = props;
|
||||||
|
const { comment_id, author_uri } = comment || {};
|
||||||
|
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
const reactionKey = activeChannelId ? `${comment_id}:${activeChannelId}` : comment_id;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
myChannelIds: selectMyClaimIdsRaw(state),
|
||||||
thumbnail: props.authorUri && selectThumbnailForUri(state, props.authorUri),
|
claim: makeSelectClaimForUri(uri)(state),
|
||||||
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
thumbnail: author_uri && selectThumbnailForUri(state, author_uri),
|
||||||
commentingEnabled: true,
|
channelIsBlocked: author_uri && makeSelectChannelIsMuted(author_uri)(state),
|
||||||
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
hasChannels: selectHasChannels(state),
|
hasChannels: selectHasChannels(state),
|
||||||
playingUri: selectPlayingUri(state),
|
playingUri: selectPlayingUri(state),
|
||||||
stakedLevel: selectTotalStakedAmountForChannelUri(state, props.authorUri),
|
stakedLevel: selectStakedLevelForChannelUri(state, author_uri),
|
||||||
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||||
totalReplyPages: makeSelectTotalReplyPagesForParentId(props.commentId)(state),
|
totalReplyPages: makeSelectTotalReplyPagesForParentId(comment_id)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = {
|
||||||
clearPlayingUri: () => dispatch(doSetPlayingUri({ uri: null })),
|
doClearPlayingUri,
|
||||||
updateComment: (commentId, comment) => dispatch(doCommentUpdate(commentId, comment)),
|
doCommentUpdate,
|
||||||
fetchReplies: (uri, parentId, page, pageSize, sortBy) =>
|
fetchReplies: doCommentList,
|
||||||
dispatch(doCommentList(uri, parentId, page, pageSize, sortBy)),
|
doToast,
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
};
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(Comment);
|
export default connect(select, perform)(Comment);
|
||||||
|
|
|
@ -12,7 +12,7 @@ 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 Tooltip from 'component/common/tooltip';
|
import CommentBadge from 'component/common/comment-badge'; // have this?
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { Menu, MenuButton } from '@reach/menu-button';
|
import { Menu, MenuButton } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
|
@ -27,23 +27,21 @@ import CommentMenuList from 'component/commentMenuList';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
import { getChannelFromClaim } from 'util/claim';
|
||||||
import { parseSticker } from 'util/comments';
|
import { parseSticker } from 'util/comments';
|
||||||
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
|
||||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
comment: Comment,
|
||||||
|
myChannelIds: ?Array<string>,
|
||||||
clearPlayingUri: () => void,
|
clearPlayingUri: () => void,
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
author: ?string, // LBRY Channel Name, e.g. @channel
|
|
||||||
authorUri: string, // full LBRY Channel URI: lbry://@channel#123...
|
|
||||||
commentId: string, // sha256 digest identifying the comment
|
|
||||||
message: string, // comment body
|
|
||||||
timePosted: number, // Comment timestamp
|
|
||||||
channelIsBlocked: boolean, // if the channel is blacklisted in the app
|
channelIsBlocked: boolean, // if the channel is blacklisted in the app
|
||||||
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
claimIsMine: boolean, // if you control the claim which this comment was posted on
|
||||||
commentIsMine: boolean, // if this comment was signed by an owned channel
|
doCommentUpdate: (string, string) => void,
|
||||||
updateComment: (string, string) => void,
|
|
||||||
fetchReplies: (string, string, number, number, number) => void,
|
fetchReplies: (string, string, number, number, number) => void,
|
||||||
totalReplyPages: number,
|
totalReplyPages: number,
|
||||||
commentModBlock: (string) => void,
|
commentModBlock: (string) => void,
|
||||||
|
@ -55,7 +53,6 @@ type Props = {
|
||||||
isTopLevel?: boolean,
|
isTopLevel?: boolean,
|
||||||
threadDepth: number,
|
threadDepth: number,
|
||||||
hideActions?: boolean,
|
hideActions?: boolean,
|
||||||
isPinned: boolean,
|
|
||||||
othersReacts: ?{
|
othersReacts: ?{
|
||||||
like: number,
|
like: number,
|
||||||
dislike: number,
|
dislike: number,
|
||||||
|
@ -64,11 +61,6 @@ type Props = {
|
||||||
activeChannelClaim: ?ChannelClaim,
|
activeChannelClaim: ?ChannelClaim,
|
||||||
playingUri: ?PlayingUri,
|
playingUri: ?PlayingUri,
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
supportAmount: number,
|
|
||||||
numDirectReplies: number,
|
|
||||||
isModerator: boolean,
|
|
||||||
isGlobalMod: boolean,
|
|
||||||
isFiat: boolean,
|
|
||||||
supportDisabled: boolean,
|
supportDisabled: boolean,
|
||||||
setQuickReply: (any) => void,
|
setQuickReply: (any) => void,
|
||||||
quickReply: any,
|
quickReply: any,
|
||||||
|
@ -76,19 +68,15 @@ type Props = {
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
|
||||||
function Comment(props: Props) {
|
function CommentView(props: Props) {
|
||||||
const {
|
const {
|
||||||
|
comment,
|
||||||
|
myChannelIds,
|
||||||
clearPlayingUri,
|
clearPlayingUri,
|
||||||
claim,
|
claim,
|
||||||
uri,
|
uri,
|
||||||
author,
|
|
||||||
authorUri,
|
|
||||||
timePosted,
|
|
||||||
message,
|
|
||||||
channelIsBlocked,
|
channelIsBlocked,
|
||||||
commentIsMine,
|
doCommentUpdate,
|
||||||
commentId,
|
|
||||||
updateComment,
|
|
||||||
fetchReplies,
|
fetchReplies,
|
||||||
totalReplyPages,
|
totalReplyPages,
|
||||||
linkedCommentId,
|
linkedCommentId,
|
||||||
|
@ -99,39 +87,61 @@ function Comment(props: Props) {
|
||||||
isTopLevel,
|
isTopLevel,
|
||||||
threadDepth,
|
threadDepth,
|
||||||
hideActions,
|
hideActions,
|
||||||
isPinned,
|
|
||||||
othersReacts,
|
othersReacts,
|
||||||
playingUri,
|
playingUri,
|
||||||
stakedLevel,
|
stakedLevel,
|
||||||
supportAmount,
|
|
||||||
numDirectReplies,
|
|
||||||
isModerator,
|
|
||||||
isGlobalMod,
|
|
||||||
isFiat,
|
|
||||||
supportDisabled,
|
supportDisabled,
|
||||||
setQuickReply,
|
setQuickReply,
|
||||||
quickReply,
|
quickReply,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
channel_url: authorUri,
|
||||||
|
channel_name: author,
|
||||||
|
channel_id: channelId,
|
||||||
|
comment_id: commentId,
|
||||||
|
comment: message,
|
||||||
|
is_fiat: isFiat,
|
||||||
|
is_global_mod: isGlobalMod,
|
||||||
|
is_moderator: isModerator,
|
||||||
|
is_pinned: isPinned,
|
||||||
|
support_amount: supportAmount,
|
||||||
|
replies: numDirectReplies,
|
||||||
|
timestamp,
|
||||||
|
} = comment;
|
||||||
|
|
||||||
|
const timePosted = timestamp * 1000;
|
||||||
|
const commentIsMine = channelId && myChannelIds && myChannelIds.includes(channelId);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
replace,
|
replace,
|
||||||
location: { pathname, search },
|
location: { pathname, search },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
|
||||||
|
const isLinkedComment = linkedCommentId && linkedCommentId === commentId;
|
||||||
|
const isInLinkedCommentChain =
|
||||||
|
linkedCommentId &&
|
||||||
|
linkedCommentAncestors[linkedCommentId] &&
|
||||||
|
linkedCommentAncestors[linkedCommentId].includes(commentId);
|
||||||
|
const showRepliesOnMount = isInLinkedCommentChain || AUTO_EXPAND_ALL_REPLIES;
|
||||||
|
|
||||||
const [isReplying, setReplying] = React.useState(false);
|
const [isReplying, setReplying] = React.useState(false);
|
||||||
const [isEditing, setEditing] = useState(false);
|
const [isEditing, setEditing] = useState(false);
|
||||||
const [editedMessage, setCommentValue] = useState(message);
|
const [editedMessage, setCommentValue] = useState(message);
|
||||||
const [charCount, setCharCount] = useState(editedMessage.length);
|
const [charCount, setCharCount] = useState(editedMessage.length);
|
||||||
const [showReplies, setShowReplies] = useState(false);
|
const [showReplies, setShowReplies] = useState(showRepliesOnMount); // on mount
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(showRepliesOnMount ? 1 : 0);
|
||||||
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
const [displayDeadComment, setDisplayDeadComment] = React.useState(false);
|
||||||
const likesCount = (othersReacts && othersReacts.like) || 0;
|
const likesCount = (othersReacts && othersReacts.like) || 0;
|
||||||
const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
|
const dislikesCount = (othersReacts && othersReacts.dislike) || 0;
|
||||||
const totalLikesAndDislikes = likesCount + dislikesCount;
|
const totalLikesAndDislikes = likesCount + dislikesCount;
|
||||||
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
const slimedToDeath = totalLikesAndDislikes >= 5 && dislikesCount / totalLikesAndDislikes > 0.8;
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const contentChannelClaim = getChannelFromClaim(claim);
|
||||||
|
const commentByOwnerOfContent = contentChannelClaim && contentChannelClaim.permanent_url === authorUri;
|
||||||
const stickerFromMessage = parseSticker(message);
|
const stickerFromMessage = parseSticker(message);
|
||||||
|
|
||||||
let channelOwnerOfContent;
|
let channelOwnerOfContent;
|
||||||
|
@ -142,19 +152,6 @@ function Comment(props: Props) {
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
// Auto-expand (limited to linked-comments for now, but can be for all)
|
|
||||||
useEffect(() => {
|
|
||||||
const isInLinkedCommentChain =
|
|
||||||
linkedCommentId &&
|
|
||||||
linkedCommentAncestors[linkedCommentId] &&
|
|
||||||
linkedCommentAncestors[linkedCommentId].includes(commentId);
|
|
||||||
|
|
||||||
if (isInLinkedCommentChain || AUTO_EXPAND_ALL_REPLIES) {
|
|
||||||
setShowReplies(true);
|
|
||||||
setPage(1);
|
|
||||||
}
|
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
setCharCount(editedMessage.length);
|
setCharCount(editedMessage.length);
|
||||||
|
@ -193,7 +190,7 @@ function Comment(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
updateComment(commentId, editedMessage);
|
doCommentUpdate(commentId, editedMessage);
|
||||||
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
|
if (setQuickReply) setQuickReply({ ...quickReply, comment_id: commentId, comment: editedMessage });
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
|
@ -214,6 +211,34 @@ function Comment(props: Props) {
|
||||||
replace(`${pathname}?${urlParams.toString()}`);
|
replace(`${pathname}?${urlParams.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find a way to enable linked comments - probably use share url.
|
||||||
|
// const linkedCommentRef = React.useCallback(
|
||||||
|
// (node) => {
|
||||||
|
// if (node !== null && window.pendingLinkedCommentScroll) {
|
||||||
|
// const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
||||||
|
// delete window.pendingLinkedCommentScroll;
|
||||||
|
//
|
||||||
|
// const mobileChatElem = document.querySelector('.MuiPaper-root .card--enable-overflow');
|
||||||
|
// const drawerElem = document.querySelector('.MuiDrawer-root');
|
||||||
|
// const elem = (isMobile && mobileChatElem) || window;
|
||||||
|
//
|
||||||
|
// if (elem) {
|
||||||
|
// // $FlowFixMe
|
||||||
|
// elem.scrollTo({
|
||||||
|
// top:
|
||||||
|
// node.getBoundingClientRect().top +
|
||||||
|
// // $FlowFixMe
|
||||||
|
// (mobileChatElem && drawerElem ? drawerElem.getBoundingClientRect().top * -1 : elem.scrollY) -
|
||||||
|
// ROUGH_HEADER_HEIGHT,
|
||||||
|
// left: 0,
|
||||||
|
// behavior: 'smooth',
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// [isMobile]
|
||||||
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={classnames('comment', {
|
className={classnames('comment', {
|
||||||
|
@ -225,7 +250,7 @@ function Comment(props: Props) {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classnames('comment__content', {
|
className={classnames('comment__content', {
|
||||||
[COMMENT_HIGHLIGHTED]: linkedCommentId && linkedCommentId === commentId,
|
[COMMENT_HIGHLIGHTED]: isLinkedComment,
|
||||||
'comment--slimed': slimedToDeath && !displayDeadComment,
|
'comment--slimed': slimedToDeath && !displayDeadComment,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -240,21 +265,8 @@ function Comment(props: Props) {
|
||||||
<div className="comment__body-container">
|
<div className="comment__body-container">
|
||||||
<div className="comment__meta">
|
<div className="comment__meta">
|
||||||
<div className="comment__meta-information">
|
<div className="comment__meta-information">
|
||||||
{isGlobalMod && (
|
{isGlobalMod && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_MOD} />}
|
||||||
<Tooltip title={__('Admin')}>
|
{isModerator && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_MOD} />}
|
||||||
<span className="comment__badge comment__badge--global-mod">
|
|
||||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isModerator && (
|
|
||||||
<Tooltip title={__('Moderator')}>
|
|
||||||
<span className="comment__badge comment__badge--mod">
|
|
||||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!author ? (
|
{!author ? (
|
||||||
<span className="comment__author">{__('Anonymous')}</span>
|
<span className="comment__author">{__('Anonymous')}</span>
|
||||||
|
@ -314,6 +326,7 @@ function Comment(props: Props) {
|
||||||
charCount={charCount}
|
charCount={charCount}
|
||||||
onChange={handleEditMessageChanged}
|
onChange={handleEditMessageChanged}
|
||||||
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={FF_MAX_CHARS_IN_COMMENT}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
/>
|
/>
|
||||||
<div className="section__actions section__actions--no-margin">
|
<div className="section__actions section__actions--no-margin">
|
||||||
<Button button="primary" type="submit" label={__('Done')} disabled={message === editedMessage} />
|
<Button button="primary" type="submit" label={__('Done')} disabled={message === editedMessage} />
|
||||||
|
@ -358,6 +371,7 @@ function Comment(props: Props) {
|
||||||
className="comment__action"
|
className="comment__action"
|
||||||
onClick={handleCommentReply}
|
onClick={handleCommentReply}
|
||||||
icon={ICONS.REPLY}
|
icon={ICONS.REPLY}
|
||||||
|
iconSize={isMobile && 12}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||||
|
@ -431,4 +445,4 @@ function Comment(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Comment;
|
export default CommentView;
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
selectClaimIsMine,
|
selectClaimIsMine,
|
||||||
selectFetchingMyChannels,
|
selectFetchingMyChannels,
|
||||||
selectMyClaimIdsRaw,
|
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import {
|
import {
|
||||||
selectTopLevelCommentsForUri,
|
selectTopLevelCommentsForUri,
|
||||||
|
@ -22,12 +21,17 @@ import {
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const claim = selectClaimForUri(state, props.uri);
|
const { uri } = props;
|
||||||
|
|
||||||
|
const claim = selectClaimForUri(state, uri);
|
||||||
|
const channelId = getChannelIdFromClaim(claim);
|
||||||
|
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
const topLevelComments = selectTopLevelCommentsForUri(state, props.uri);
|
const topLevelComments = selectTopLevelCommentsForUri(state, uri);
|
||||||
|
|
||||||
const resolvedComments =
|
const resolvedComments =
|
||||||
topLevelComments && topLevelComments.length > 0
|
topLevelComments && topLevelComments.length > 0
|
||||||
|
@ -37,12 +41,12 @@ const select = (state, props) => {
|
||||||
return {
|
return {
|
||||||
topLevelComments,
|
topLevelComments,
|
||||||
resolvedComments,
|
resolvedComments,
|
||||||
myChannelIds: selectMyClaimIdsRaw(state),
|
allCommentIds: selectCommentIdsForUri(state, uri),
|
||||||
allCommentIds: selectCommentIdsForUri(state, props.uri),
|
pinnedComments: selectPinnedCommentsForUri(state, uri),
|
||||||
pinnedComments: selectPinnedCommentsForUri(state, props.uri),
|
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
||||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
|
||||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
claimId: claim && claim.claim_id,
|
||||||
claim,
|
channelId,
|
||||||
claimIsMine: selectClaimIsMine(state, claim),
|
claimIsMine: selectClaimIsMine(state, claim),
|
||||||
isFetchingComments: selectIsFetchingComments(state),
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
isFetchingCommentsById: selectIsFetchingCommentsById(state),
|
isFetchingCommentsById: selectIsFetchingCommentsById(state),
|
||||||
|
@ -55,12 +59,12 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = {
|
||||||
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
fetchTopLevelComments: doCommentList,
|
||||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
fetchComment: doCommentById,
|
||||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
fetchReacts: doCommentReactList,
|
||||||
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
resetComments: doCommentReset,
|
||||||
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
|
doResolveUris,
|
||||||
});
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { COMMENT_HIGHLIGHTED } from 'constants/classnames';
|
|
||||||
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL, SORT_BY } from 'constants/comment';
|
||||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
|
import { getCommentsListTitle } from 'util/comments';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
@ -15,7 +14,6 @@ import debounce from 'util/debounce';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import useFetched from 'effects/use-fetched';
|
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
@ -32,11 +30,12 @@ type Props = {
|
||||||
allCommentIds: any,
|
allCommentIds: any,
|
||||||
pinnedComments: Array<Comment>,
|
pinnedComments: Array<Comment>,
|
||||||
topLevelComments: Array<Comment>,
|
topLevelComments: Array<Comment>,
|
||||||
|
resolvedComments: Array<Comment>,
|
||||||
topLevelTotalPages: number,
|
topLevelTotalPages: number,
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: ?Claim,
|
claimId?: string,
|
||||||
|
channelId?: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
|
||||||
isFetchingComments: boolean,
|
isFetchingComments: boolean,
|
||||||
isFetchingCommentsById: boolean,
|
isFetchingCommentsById: boolean,
|
||||||
isFetchingReacts: boolean,
|
isFetchingReacts: boolean,
|
||||||
|
@ -47,25 +46,26 @@ type Props = {
|
||||||
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||||
activeChannelId: ?string,
|
activeChannelId: ?string,
|
||||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||||
fetchReacts: (Array<string>) => Promise<any>,
|
|
||||||
commentsAreExpanded?: boolean,
|
commentsAreExpanded?: boolean,
|
||||||
fetchTopLevelComments: (string, number, number, number) => void,
|
fetchTopLevelComments: (uri: string, parentId: string, page: number, pageSize: number, sortBy: number) => void,
|
||||||
fetchComment: (string) => void,
|
fetchComment: (commentId: string) => void,
|
||||||
resetComments: (string) => void,
|
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||||
|
resetComments: (claimId: string) => void,
|
||||||
|
doResolveUris: (uris: Array<string>, returnCachedClaims: boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentList(props: Props) {
|
export default function CommentList(props: Props) {
|
||||||
const {
|
const {
|
||||||
allCommentIds,
|
allCommentIds,
|
||||||
uri,
|
uri,
|
||||||
pinnedComments,
|
pinnedComments,
|
||||||
topLevelComments,
|
topLevelComments,
|
||||||
|
resolvedComments,
|
||||||
topLevelTotalPages,
|
topLevelTotalPages,
|
||||||
claim,
|
claimId,
|
||||||
|
channelId,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
myChannels,
|
|
||||||
isFetchingComments,
|
isFetchingComments,
|
||||||
isFetchingCommentsById,
|
|
||||||
isFetchingReacts,
|
isFetchingReacts,
|
||||||
linkedCommentId,
|
linkedCommentId,
|
||||||
totalComments,
|
totalComments,
|
||||||
|
@ -74,28 +74,33 @@ function CommentList(props: Props) {
|
||||||
othersReactsById,
|
othersReactsById,
|
||||||
activeChannelId,
|
activeChannelId,
|
||||||
settingsByChannelId,
|
settingsByChannelId,
|
||||||
fetchReacts,
|
|
||||||
commentsAreExpanded,
|
commentsAreExpanded,
|
||||||
fetchTopLevelComments,
|
fetchTopLevelComments,
|
||||||
fetchComment,
|
fetchComment,
|
||||||
|
fetchReacts,
|
||||||
resetComments,
|
resetComments,
|
||||||
|
doResolveUris,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMediumScreen = useIsMediumScreen();
|
const isMediumScreen = useIsMediumScreen();
|
||||||
|
|
||||||
const spinnerRef = React.useRef();
|
const spinnerRef = React.useRef();
|
||||||
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
|
||||||
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
|
||||||
const [page, setPage] = React.useState(0);
|
const [page, setPage] = React.useState(0);
|
||||||
const fetchedCommentsOnce = useFetched(isFetchingComments);
|
const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
|
||||||
const fetchedReactsOnce = useFetched(isFetchingReacts);
|
const [didInitialPageFetch, setInitialPageFetch] = React.useState(false);
|
||||||
const fetchedLinkedComment = useFetched(isFetchingCommentsById);
|
const hasDefaultExpansion = commentsAreExpanded || !isMediumScreen || isMobile;
|
||||||
const hasDefaultExpansion = commentsAreExpanded || (!isMobile && !isMediumScreen);
|
|
||||||
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
|
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
|
||||||
|
|
||||||
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||||
const channelId = getChannelIdFromClaim(claim);
|
|
||||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||||
const moreBelow = page < topLevelTotalPages;
|
const moreBelow = page < topLevelTotalPages;
|
||||||
|
const isResolvingComments = topLevelComments && resolvedComments.length !== topLevelComments.length;
|
||||||
|
const alreadyResolved = !isResolvingComments && resolvedComments.length !== 0;
|
||||||
|
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === topLevelComments.length;
|
||||||
|
const title = getCommentsListTitle(totalComments);
|
||||||
|
|
||||||
// Display comments immediately if not fetching reactions
|
// Display comments immediately if not fetching reactions
|
||||||
// If not, wait to show comments until reactions are fetched
|
// If not, wait to show comments until reactions are fetched
|
||||||
|
@ -113,8 +118,8 @@ function CommentList(props: Props) {
|
||||||
// Reset comments
|
// Reset comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
if (claim) {
|
if (claimId) {
|
||||||
resetComments(claim.claim_id);
|
resetComments(claimId);
|
||||||
}
|
}
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
|
@ -128,7 +133,7 @@ function CommentList(props: Props) {
|
||||||
fetchComment(linkedCommentId);
|
fetchComment(linkedCommentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTopLevelComments(uri, page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
fetchTopLevelComments(uri, '', page, COMMENT_PAGE_SIZE_TOP_LEVEL, sort);
|
||||||
}
|
}
|
||||||
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]);
|
}, [fetchComment, fetchTopLevelComments, linkedCommentId, page, sort, uri]);
|
||||||
|
|
||||||
|
@ -167,23 +172,17 @@ function CommentList(props: Props) {
|
||||||
|
|
||||||
// Scroll to linked-comment
|
// Scroll to linked-comment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetchedLinkedComment && fetchedCommentsOnce && fetchedReactsOnce) {
|
if (linkedCommentId) {
|
||||||
const elems = document.getElementsByClassName(COMMENT_HIGHLIGHTED);
|
window.pendingLinkedCommentScroll = true;
|
||||||
if (elems.length > 0) {
|
} else {
|
||||||
const ROUGH_HEADER_HEIGHT = 125; // @see: --header-height
|
delete window.pendingLinkedCommentScroll;
|
||||||
const linkedComment = elems[0];
|
|
||||||
window.scrollTo({
|
|
||||||
top: linkedComment.getBoundingClientRect().top + window.scrollY - ROUGH_HEADER_HEIGHT,
|
|
||||||
left: 0,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [fetchedLinkedComment, fetchedCommentsOnce, fetchedReactsOnce]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Infinite scroll
|
// Infinite scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function shouldFetchNextPage(page, topLevelTotalPages, window, document, yPrefetchPx = 1000) {
|
function shouldFetchNextPage(page, topLevelTotalPages, yPrefetchPx = 1000) {
|
||||||
if (!spinnerRef || !spinnerRef.current) return false;
|
if (!spinnerRef || !spinnerRef.current) return false;
|
||||||
|
|
||||||
const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe
|
const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe
|
||||||
|
@ -206,86 +205,67 @@ function CommentList(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommentScroll = debounce(() => {
|
const handleCommentScroll = debounce(() => {
|
||||||
if (hasDefaultExpansion && shouldFetchNextPage(page, topLevelTotalPages, window, document)) {
|
if (shouldFetchNextPage(page, topLevelTotalPages)) {
|
||||||
setPage(page + 1);
|
setPage(page + 1);
|
||||||
|
setInitialPageFetch(true);
|
||||||
}
|
}
|
||||||
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
}, DEBOUNCE_SCROLL_HANDLER_MS);
|
||||||
|
|
||||||
if (!isFetchingComments && readyToDisplayComments && moreBelow && spinnerRef && spinnerRef.current) {
|
if (!didInitialPageFetch) {
|
||||||
if (shouldFetchNextPage(page, topLevelTotalPages, window, document, 0)) {
|
handleCommentScroll();
|
||||||
setPage(page + 1);
|
setInitialPageFetch(true);
|
||||||
} else {
|
}
|
||||||
window.addEventListener('scroll', handleCommentScroll);
|
|
||||||
return () => window.removeEventListener('scroll', handleCommentScroll);
|
if (hasDefaultExpansion && !isFetchingComments && canDisplayComments && readyToDisplayComments && moreBelow) {
|
||||||
|
const commentsInDrawer = Boolean(document.querySelector('.MuiDrawer-root .card--enable-overflow'));
|
||||||
|
const scrollingElement = commentsInDrawer ? document.querySelector('.card--enable-overflow') : window;
|
||||||
|
|
||||||
|
if (scrollingElement) {
|
||||||
|
scrollingElement.addEventListener('scroll', handleCommentScroll);
|
||||||
|
|
||||||
|
return () => scrollingElement.removeEventListener('scroll', handleCommentScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [hasDefaultExpansion, isFetchingComments, moreBelow, page, readyToDisplayComments, topLevelTotalPages]);
|
}, [
|
||||||
|
canDisplayComments,
|
||||||
|
hasDefaultExpansion,
|
||||||
|
didInitialPageFetch,
|
||||||
|
isFetchingComments,
|
||||||
|
isMobile,
|
||||||
|
moreBelow,
|
||||||
|
page,
|
||||||
|
readyToDisplayComments,
|
||||||
|
topLevelTotalPages,
|
||||||
|
]);
|
||||||
|
|
||||||
const getCommentElems = (comments) => {
|
// Wait to only display topLevelComments after resolved or else
|
||||||
return comments.map((comment) => (
|
// other components will try to resolve again, like channelThumbnail
|
||||||
<CommentView
|
useEffect(() => {
|
||||||
isTopLevel
|
if (!isResolvingComments) setCommentsToDisplay(topLevelComments);
|
||||||
threadDepth={3}
|
}, [isResolvingComments, topLevelComments]);
|
||||||
key={comment.comment_id}
|
|
||||||
uri={uri}
|
|
||||||
authorUri={comment.channel_url}
|
|
||||||
author={comment.channel_name}
|
|
||||||
claimId={comment.claim_id}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
timePosted={comment.timestamp * 1000}
|
|
||||||
claimIsMine={claimIsMine}
|
|
||||||
commentIsMine={
|
|
||||||
comment.channel_id && myChannels && myChannels.some(({ claim_id }) => claim_id === comment.channel_id)
|
|
||||||
}
|
|
||||||
linkedCommentId={linkedCommentId}
|
|
||||||
isPinned={comment.is_pinned}
|
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
numDirectReplies={comment.replies}
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
isFiat={comment.is_fiat}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
const sortButton = (label, icon, sortOption) => {
|
// Batch resolve comment channel urls
|
||||||
return (
|
useEffect(() => {
|
||||||
<Button
|
if (!topLevelComments || alreadyResolved) return;
|
||||||
button="alt"
|
|
||||||
label={label}
|
const urisToResolve = [];
|
||||||
icon={icon}
|
topLevelComments.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
|
||||||
iconSize={18}
|
|
||||||
onClick={() => changeSort(sortOption)}
|
if (urisToResolve.length > 0) doResolveUris(urisToResolve, true);
|
||||||
className={classnames(`button-toggle`, {
|
}, [alreadyResolved, doResolveUris, topLevelComments]);
|
||||||
'button-toggle--active': sort === sortOption,
|
|
||||||
})}
|
const commentProps = { isTopLevel: true, threadDepth: 3, uri, claimIsMine, linkedCommentId };
|
||||||
/>
|
const actionButtonsProps = { totalComments, sort, changeSort, setPage };
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="card--enable-overflow"
|
className="card--enable-overflow"
|
||||||
title={
|
title={!isMobile && title}
|
||||||
(totalComments === 0 && __('Leave a comment')) ||
|
titleActions={<CommentActionButtons {...actionButtonsProps} />}
|
||||||
(totalComments === 1 && __('1 comment')) ||
|
|
||||||
__('%totalComments% comments', { totalComments })
|
|
||||||
}
|
|
||||||
titleActions={
|
|
||||||
<>
|
|
||||||
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
|
||||||
<span className="comment__sort">
|
|
||||||
{sortButton(__('Best'), ICONS.BEST, SORT_BY.POPULARITY)}
|
|
||||||
{sortButton(__('Controversial'), ICONS.CONTROVERSIAL, SORT_BY.CONTROVERSY)}
|
|
||||||
{sortButton(__('New'), ICONS.NEW, SORT_BY.NEWEST)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
|
{isMobile && <CommentActionButtons {...actionButtonsProps} />}
|
||||||
|
|
||||||
<CommentCreate uri={uri} />
|
<CommentCreate uri={uri} />
|
||||||
|
|
||||||
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
|
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && !totalComments && (
|
||||||
|
@ -294,15 +274,20 @@ function CommentList(props: Props) {
|
||||||
|
|
||||||
<ul
|
<ul
|
||||||
className={classnames({
|
className={classnames({
|
||||||
comments: expandedComments,
|
comments: !isMediumScreen || expandedComments,
|
||||||
'comments--contracted': !expandedComments,
|
'comments--contracted': isMediumScreen && !expandedComments,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{readyToDisplayComments && pinnedComments && getCommentElems(pinnedComments)}
|
{readyToDisplayComments && (
|
||||||
{readyToDisplayComments && topLevelComments && getCommentElems(topLevelComments)}
|
<>
|
||||||
|
{pinnedComments && <CommentElements comments={pinnedComments} {...commentProps} />}
|
||||||
|
|
||||||
|
{commentsToDisplay && <CommentElements comments={commentsToDisplay} {...commentProps} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{!hasDefaultExpansion && topLevelComments && Boolean(topLevelComments.length) && (
|
{!hasDefaultExpansion && (
|
||||||
<div className="card__bottom-actions--comments">
|
<div className="card__bottom-actions--comments">
|
||||||
{(!expandedComments || moreBelow) && (
|
{(!expandedComments || moreBelow) && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -323,7 +308,7 @@ function CommentList(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isFetchingComments || (hasDefaultExpansion && moreBelow)) && (
|
{(isFetchingComments || (hasDefaultExpansion && moreBelow) || !canDisplayComments) && (
|
||||||
<div className="main--empty" ref={spinnerRef}>
|
<div className="main--empty" ref={spinnerRef}>
|
||||||
<Spinner type="small" />
|
<Spinner type="small" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -334,4 +319,64 @@ function CommentList(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CommentList;
|
type CommentProps = {
|
||||||
|
comments: Array<Comment>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentElements = (commentProps: CommentProps) => {
|
||||||
|
const { comments, ...commentsProps } = commentProps;
|
||||||
|
|
||||||
|
return comments.map((comment) => <CommentView key={comment.comment_id} comment={comment} {...commentsProps} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionButtonsProps = {
|
||||||
|
totalComments: number,
|
||||||
|
sort: string,
|
||||||
|
changeSort: (string) => void,
|
||||||
|
setPage: (number) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentActionButtons = (actionButtonsProps: ActionButtonsProps) => {
|
||||||
|
const { totalComments, sort, changeSort, setPage } = actionButtonsProps;
|
||||||
|
|
||||||
|
const sortButtonProps = { activeSort: sort, changeSort };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
|
||||||
|
<span className="comment__sort">
|
||||||
|
<SortButton {...sortButtonProps} label={__('Best')} icon={ICONS.BEST} sortOption={SORT_BY.POPULARITY} />
|
||||||
|
<SortButton
|
||||||
|
{...sortButtonProps}
|
||||||
|
label={__('Controversial')}
|
||||||
|
icon={ICONS.CONTROVERSIAL}
|
||||||
|
sortOption={SORT_BY.CONTROVERSY}
|
||||||
|
/>
|
||||||
|
<SortButton {...sortButtonProps} label={__('New')} icon={ICONS.NEW} sortOption={SORT_BY.NEWEST} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button button="alt" icon={ICONS.REFRESH} title={__('Refresh')} onClick={() => setPage(0)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SortButtonProps = {
|
||||||
|
activeSort: string,
|
||||||
|
sortOption: string,
|
||||||
|
changeSort: (string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortButton = (sortButtonProps: SortButtonProps) => {
|
||||||
|
const { activeSort, sortOption, changeSort, ...buttonProps } = sortButtonProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...buttonProps}
|
||||||
|
className={classnames(`button-toggle`, { 'button-toggle--active': activeSort === sortOption })}
|
||||||
|
button="alt"
|
||||||
|
iconSize={18}
|
||||||
|
onClick={() => changeSort(sortOption)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doResolveUris } from 'redux/actions/claims';
|
import { doResolveUris } from 'redux/actions/claims';
|
||||||
import { makeSelectClaimIsMine, selectMyChannelClaims, makeSelectClaimForUri } from 'redux/selectors/claims';
|
import { selectClaimIsMineForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments';
|
import { selectIsFetchingCommentsByParentId, selectRepliesForParentId } from 'redux/selectors/comments';
|
||||||
import CommentsReplies from './view';
|
import CommentsReplies from './view';
|
||||||
|
|
||||||
|
@ -14,9 +14,8 @@ const select = (state, props) => {
|
||||||
return {
|
return {
|
||||||
fetchedReplies,
|
fetchedReplies,
|
||||||
resolvedReplies,
|
resolvedReplies,
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: selectClaimIsMineForUri(state, props.uri),
|
||||||
userCanComment: true,
|
userCanComment: true,
|
||||||
myChannels: selectMyChannelClaims(state),
|
|
||||||
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
isFetchingByParentId: selectIsFetchingCommentsByParentId(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
|
||||||
import Comment from 'component/comment';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import Comment from 'component/comment';
|
||||||
|
import React from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
fetchedReplies: Array<any>,
|
fetchedReplies: Array<Comment>,
|
||||||
|
resolvedReplies: Array<Comment>,
|
||||||
uri: string,
|
uri: string,
|
||||||
parentId: string,
|
parentId: string,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
|
||||||
linkedCommentId?: string,
|
linkedCommentId?: string,
|
||||||
commentingEnabled: boolean,
|
userCanComment: boolean,
|
||||||
threadDepth: number,
|
threadDepth: number,
|
||||||
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
|
numDirectReplies: number, // Total replies for parentId as reported by 'comment[replies]'. Includes blocked items.
|
||||||
isFetchingByParentId: { [string]: boolean },
|
isFetchingByParentId: { [string]: boolean },
|
||||||
onShowMore?: () => void,
|
|
||||||
hasMore: boolean,
|
hasMore: boolean,
|
||||||
supportDisabled: boolean,
|
supportDisabled: boolean,
|
||||||
|
doResolveUris: (Array<string>) => void,
|
||||||
|
onShowMore?: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CommentsReplies(props: Props) {
|
function CommentsReplies(props: Props) {
|
||||||
|
@ -26,102 +27,93 @@ function CommentsReplies(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
parentId,
|
parentId,
|
||||||
fetchedReplies,
|
fetchedReplies,
|
||||||
|
resolvedReplies,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
myChannels,
|
|
||||||
linkedCommentId,
|
linkedCommentId,
|
||||||
commentingEnabled,
|
userCanComment,
|
||||||
threadDepth,
|
threadDepth,
|
||||||
numDirectReplies,
|
numDirectReplies,
|
||||||
isFetchingByParentId,
|
isFetchingByParentId,
|
||||||
onShowMore,
|
|
||||||
hasMore,
|
hasMore,
|
||||||
supportDisabled,
|
supportDisabled,
|
||||||
|
doResolveUris,
|
||||||
|
onShowMore,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [isExpanded, setExpanded] = React.useState(true);
|
const [isExpanded, setExpanded] = React.useState(true);
|
||||||
|
const [commentsToDisplay, setCommentsToDisplay] = React.useState(fetchedReplies);
|
||||||
|
const isResolvingReplies = fetchedReplies && resolvedReplies.length !== fetchedReplies.length;
|
||||||
|
const alreadyResolved = !isResolvingReplies && resolvedReplies.length !== 0;
|
||||||
|
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === fetchedReplies.length;
|
||||||
|
|
||||||
function showMore() {
|
// Batch resolve comment channel urls
|
||||||
if (onShowMore) {
|
React.useEffect(() => {
|
||||||
onShowMore();
|
if (!fetchedReplies || alreadyResolved) return;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
const urisToResolve = [];
|
||||||
function isMyComment(channelId: string) {
|
fetchedReplies.map(({ channel_url }) => channel_url !== undefined && urisToResolve.push(channel_url));
|
||||||
if (myChannels != null && channelId != null) {
|
|
||||||
for (let i = 0; i < myChannels.length; i++) {
|
|
||||||
if (myChannels[i].claim_id === channelId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayedComments = fetchedReplies;
|
if (urisToResolve.length > 0) doResolveUris(urisToResolve);
|
||||||
|
}, [alreadyResolved, doResolveUris, fetchedReplies]);
|
||||||
|
|
||||||
return (
|
// Wait to only display topLevelComments after resolved or else
|
||||||
Boolean(numDirectReplies) && (
|
// other components will try to resolve again, like channelThumbnail
|
||||||
<div className="comment__replies-container">
|
React.useEffect(() => {
|
||||||
{Boolean(numDirectReplies) && !isExpanded && (
|
if (!isResolvingReplies) setCommentsToDisplay(fetchedReplies);
|
||||||
|
}, [isResolvingReplies, fetchedReplies]);
|
||||||
|
|
||||||
|
return !numDirectReplies ? null : (
|
||||||
|
<div className="comment__replies-container">
|
||||||
|
{!isExpanded ? (
|
||||||
|
<div className="comment__actions--nested">
|
||||||
|
<Button
|
||||||
|
className="comment__action"
|
||||||
|
label={__('Show Replies')}
|
||||||
|
onClick={() => setExpanded(!isExpanded)}
|
||||||
|
icon={isExpanded ? ICONS.UP : ICONS.DOWN}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="comment__replies">
|
||||||
|
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||||
|
|
||||||
|
<ul className="comments--replies">
|
||||||
|
{!isResolvingReplies &&
|
||||||
|
commentsToDisplay &&
|
||||||
|
commentsToDisplay.length > 0 &&
|
||||||
|
commentsToDisplay.map((comment) => (
|
||||||
|
<Comment
|
||||||
|
key={comment.comment_id}
|
||||||
|
threadDepth={threadDepth}
|
||||||
|
uri={uri}
|
||||||
|
comment={comment}
|
||||||
|
claimIsMine={claimIsMine}
|
||||||
|
linkedCommentId={linkedCommentId}
|
||||||
|
commentingEnabled={userCanComment}
|
||||||
|
supportDisabled={supportDisabled}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isExpanded && fetchedReplies && hasMore && (
|
||||||
|
<div className="comment__actions--nested">
|
||||||
|
<Button
|
||||||
|
button="link"
|
||||||
|
label={__('Show more')}
|
||||||
|
onClick={() => onShowMore && onShowMore()}
|
||||||
|
className="button--uri-indicator"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(isFetchingByParentId[parentId] || isResolvingReplies || !canDisplayComments) && (
|
||||||
|
<div className="comment__replies-container">
|
||||||
<div className="comment__actions--nested">
|
<div className="comment__actions--nested">
|
||||||
<Button
|
<Spinner type="small" />
|
||||||
className="comment__action"
|
|
||||||
label={__('Show Replies')}
|
|
||||||
onClick={() => setExpanded(!isExpanded)}
|
|
||||||
icon={isExpanded ? ICONS.UP : ICONS.DOWN}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{isExpanded && (
|
)}
|
||||||
<div>
|
</div>
|
||||||
<div className="comment__replies">
|
|
||||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
|
||||||
|
|
||||||
<ul className="comments--replies">
|
|
||||||
{displayedComments &&
|
|
||||||
displayedComments.map((comment) => {
|
|
||||||
return (
|
|
||||||
<Comment
|
|
||||||
threadDepth={threadDepth}
|
|
||||||
uri={uri}
|
|
||||||
authorUri={comment.channel_url}
|
|
||||||
author={comment.channel_name}
|
|
||||||
claimId={comment.claim_id}
|
|
||||||
commentId={comment.comment_id}
|
|
||||||
key={comment.comment_id}
|
|
||||||
message={comment.comment}
|
|
||||||
timePosted={comment.timestamp * 1000}
|
|
||||||
claimIsMine={claimIsMine}
|
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
|
||||||
linkedCommentId={linkedCommentId}
|
|
||||||
commentingEnabled={commentingEnabled}
|
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
numDirectReplies={comment.replies}
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
supportDisabled={supportDisabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isExpanded && fetchedReplies && hasMore && (
|
|
||||||
<div className="comment__actions--nested">
|
|
||||||
<Button button="link" label={__('Show more')} onClick={showMore} className="button--uri-indicator" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isFetchingByParentId[parentId] && (
|
|
||||||
<div className="comment__replies-container">
|
|
||||||
<div className="comment__actions--nested">
|
|
||||||
<Spinner type="small" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
ui/component/common/comment-badge.jsx
Normal file
35
ui/component/common/comment-badge.jsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import 'scss/component/_comment-badge.scss';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import Icon from 'component/common/icon';
|
||||||
|
import React from 'react';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
|
const LABEL_TYPES = {
|
||||||
|
ADMIN: 'Admin',
|
||||||
|
MOD: 'Moderator',
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon: string,
|
||||||
|
label: string,
|
||||||
|
size?: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CommentBadge(props: Props) {
|
||||||
|
const { icon, label, size = 20 } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={label} placement="top">
|
||||||
|
<span
|
||||||
|
className={classnames('comment__badge', {
|
||||||
|
'comment__badge--globalMod': label === LABEL_TYPES.ADMIN,
|
||||||
|
'comment__badge--mod': label === LABEL_TYPES.MOD,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Icon icon={icon} size={size} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
|
@ -75,21 +75,15 @@ export default function OwnComments(props: Props) {
|
||||||
)}
|
)}
|
||||||
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
|
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
|
||||||
</div>
|
</div>
|
||||||
<Comment
|
<React.Suspense fallback={null}>
|
||||||
isTopLevel
|
<Comment
|
||||||
hideActions
|
isTopLevel
|
||||||
authorUri={comment.channel_url}
|
hideActions
|
||||||
author={comment.channel_name}
|
comment={comment}
|
||||||
commentId={comment.comment_id}
|
commentIsMine
|
||||||
message={comment.comment}
|
numDirectReplies={0} // Don't show replies here
|
||||||
timePosted={comment.timestamp * 1000}
|
/>
|
||||||
commentIsMine
|
</React.Suspense>
|
||||||
supportAmount={comment.support_amount}
|
|
||||||
numDirectReplies={0} // Don't show replies here
|
|
||||||
isModerator={comment.is_moderator}
|
|
||||||
isGlobalMod={comment.is_global_mod}
|
|
||||||
isFiat={comment.is_fiat}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -110,6 +110,8 @@ export function doSetPrimaryUri(uri: ?string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const doClearPlayingUri = () => (dispatch: Dispatch) => dispatch(doSetPlayingUri({ uri: null }));
|
||||||
|
|
||||||
export function doSetPlayingUri({
|
export function doSetPlayingUri({
|
||||||
uri,
|
uri,
|
||||||
source,
|
source,
|
||||||
|
|
23
ui/scss/component/_comment-badge.scss
Normal file
23
ui/scss/component/_comment-badge.scss
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.comment__badge {
|
||||||
|
padding-right: var(--spacing-xxs);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-bottom: -4px;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__badge--globalMod {
|
||||||
|
.st0 {
|
||||||
|
// @see: ICONS.BADGE_MOD
|
||||||
|
fill: #fe7500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment__badge--mod {
|
||||||
|
.st0 {
|
||||||
|
// @see: ICONS.BADGE_MOD
|
||||||
|
fill: #ff3850;
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,3 +108,17 @@ export function parseSticker(comment: string) {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStickerUrl(comment: string) {
|
||||||
|
const stickerFromComment = parseSticker(comment);
|
||||||
|
return stickerFromComment && stickerFromComment.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommentsListTitle(totalComments: number) {
|
||||||
|
const title =
|
||||||
|
(totalComments === 0 && __('Leave a comment')) ||
|
||||||
|
(totalComments === 1 && __('1 comment')) ||
|
||||||
|
__('%total_comments% comments', { total_comments: totalComments });
|
||||||
|
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue