comment reactions

This commit is contained in:
jessop 2020-09-29 10:10:23 -04:00 committed by Sean Yesmunt
parent bdb3d695ee
commit 63ce107cc1
14 changed files with 252 additions and 16 deletions

12
flow-typed/Comment.js vendored
View file

@ -22,4 +22,16 @@ declare type CommentsState = {
commentById: { [string]: Comment },
isLoading: boolean,
myComments: ?Set<string>,
isFetchingReacts: boolean,
myReactsByCommentId: any,
othersReactsByCommentId: any,
};
declare type CommentReactParams = {
comment_ids: string,
channel_name: string,
channel_id: string,
react_type: string,
clear_types?: string,
remove?: boolean,
}

View file

@ -136,7 +136,7 @@
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#90012bf47c170f244039261548dab7c7597046dc",
"lbry-redux": "lbryio/lbry-redux#04015155796bc588bdf5b10762cfc874e6a1b00c",
"lbryinc": "lbryio/lbryinc#db0663fcc4a64cb082b6edc5798fafa67eb4300f",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",

View file

@ -1175,7 +1175,6 @@
"Uncheck your email below if you want to stop receiving messages.": "Uncheck your email below if you want to stop receiving messages.",
"Remove from blocked list": "Remove from blocked list",
"Are you sure you want to remove this from the list?": "Are you sure you want to remove this from the list?",
"Send a tip": "Send a tip",
"Send a chunk of change to this creator to let them know you appreciate their content.": "Send a chunk of change to this creator to let them know you appreciate their content.",
"CableTube Escape Artists": "CableTube Escape Artists",
"Unlink YouTube Channel": "Unlink YouTube Channel",
@ -1279,5 +1278,24 @@
"Something went wrong. Please %click_here% to learn about sync limitations.": "Something went wrong. Please %click_here% to learn about sync limitations.",
"Buy LBC": "Buy LBC",
"Continue...": "Continue...",
"Leave a comment": "Leave a comment",
"Be the first to comment!": "Be the first to comment!",
"Comment as": "Comment as",
"No uploads": "No uploads",
"You haven't uploaded anything yet. This is where you can find them when you do!": "You haven't uploaded anything yet. This is where you can find them when you do!",
"Discussion": "Discussion",
"Staked LBRY Credits": "Staked LBRY Credits",
"uploads": "uploads",
"1 comment": "1 comment",
"Upvote": "Upvote",
"Downvote": "Downvote",
"Replying as": "Replying as",
"Hide %number% Replies": "Hide %number% Replies",
"Show %number% Replies": "Show %number% Replies",
"Change to list layout": "Change to list layout",
"Create a channel": "Create a channel",
"Credit Details": "Credit Details",
"Sign In": "Sign In",
"Change to tile layout": "Change to tile layout",
"--end--": "--end--"
}

View file

@ -246,7 +246,6 @@ function Comment(props: Props) {
</div>
<div className="comment__actions">
<CommentReactions />
{!hideReplyButton && (
<Button
requiresAuth={IS_WEB}
@ -256,6 +255,7 @@ function Comment(props: Props) {
icon={ICONS.REPLY}
/>
)}
<CommentReactions commentId={commentId} />
</div>
</>
)}

View file

@ -1,8 +1,15 @@
import { connect } from 'react-redux';
import Comment from './view';
import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
import { doCommentReact } from 'redux/actions/comments';
const select = (state, props) => ({});
const select = (state, props) => ({
myReacts: makeSelectMyReactionsForComment(props.commentId)(state),
othersReacts: makeSelectOthersReactionsForComment(props.commentId)(state),
});
const perform = dispatch => ({});
const perform = dispatch => ({
react: (commentId, type) => dispatch(doCommentReact(commentId, type)),
});
export default connect(select, perform)(Comment);

View file

@ -4,13 +4,29 @@ import * as REACTION_TYPES from 'constants/reactions';
import React from 'react';
import classnames from 'classnames';
import Button from 'component/button';
import usePersistedState from 'effects/use-persisted-state';
type Props = {
myReaction: ?string,
myReacts: Array<string>,
othersReacts: any,
react: (string, string) => void,
commentId: string,
};
export default function CommentReactions(props: Props) {
const { myReaction } = props;
const { myReacts, othersReacts, commentId, react } = props;
const [activeChannel] = usePersistedState('comment-channel');
const getCountForReact = type => {
let count = 0;
if (othersReacts && othersReacts[type]) {
count += othersReacts[type];
}
if (myReacts && myReacts.includes(type)) {
count += 1;
}
return count;
};
return (
<>
@ -18,15 +34,20 @@ export default function CommentReactions(props: Props) {
title={__('Upvote')}
icon={ICONS.UPVOTE}
className={classnames('comment__action', {
'comment__action--active': myReaction === REACTION_TYPES.LIKE,
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
})}
disabled={!activeChannel}
onClick={() => react(commentId, REACTION_TYPES.LIKE)}
label={getCountForReact(REACTION_TYPES.LIKE)}
/>
<Button
title={__('Downvote')}
icon={ICONS.DOWNVOTE}
className={classnames('comment__action', {
'comment__action--active': myReaction === REACTION_TYPES.DISLIKE,
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
})}
onClick={() => react(commentId, REACTION_TYPES.DISLIKE)}
label={getCountForReact(REACTION_TYPES.DISLIKE)}
/>
</>
);

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { makeSelectClaimIsMine, selectMyChannelClaims } from 'lbry-redux';
import { makeSelectTopLevelCommentsForUri, selectIsFetchingComments } from 'redux/selectors/comments';
import { doCommentList } from 'redux/actions/comments';
import { doCommentList, doCommentReactList } from 'redux/actions/comments';
import CommentsList from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
@ -15,6 +15,7 @@ const select = (state, props) => ({
const perform = dispatch => ({
fetchComments: uri => dispatch(doCommentList(uri)),
fetchReacts: uri => dispatch(doCommentReactList(uri)),
});
export default connect(select, perform)(CommentsList);

View file

@ -5,10 +5,12 @@ import Spinner from 'component/spinner';
import Button from 'component/button';
import Card from 'component/common/card';
import CommentCreate from 'component/commentCreate';
import usePersistedState from 'effects/use-persisted-state';
type Props = {
comments: Array<any>,
fetchComments: string => void,
fetchReacts: string => void,
uri: string,
claimIsMine: boolean,
myChannels: ?Array<ChannelClaim>,
@ -17,7 +19,16 @@ type Props = {
};
function CommentList(props: Props) {
const { fetchComments, uri, comments, claimIsMine, myChannels, isFetchingComments, linkedComment } = props;
const {
fetchComments,
fetchReacts,
uri,
comments,
claimIsMine,
myChannels,
isFetchingComments,
linkedComment,
} = props;
const linkedCommentId = linkedComment && linkedComment.comment_id;
const [start] = React.useState(0);
@ -44,12 +55,19 @@ function CommentList(props: Props) {
}
};
const [activeChannel] = usePersistedState('comment-channel', '');
const commentRef = React.useRef();
useEffect(() => {
fetchComments(uri);
}, [fetchComments, uri]);
useEffect(() => {
if (totalComments) {
fetchReacts(uri);
}
}, [fetchReacts, uri, totalComments, activeChannel]);
useEffect(() => {
if (linkedCommentId && commentRef && commentRef.current) {
commentRef.current.scrollIntoView({ block: 'start' });

View file

@ -265,6 +265,12 @@ export const COMMENT_UPDATE_FAILED = 'COMMENT_UPDATE_FAILED';
export const COMMENT_HIDE_STARTED = 'COMMENT_HIDE_STARTED';
export const COMMENT_HIDE_COMPLETED = 'COMMENT_HIDE_COMPLETED';
export const COMMENT_HIDE_FAILED = 'COMMENT_HIDE_FAILED';
export const COMMENT_REACTION_LIST_STARTED = 'COMMENT_REACTION_LIST_STARTED';
export const COMMENT_REACTION_LIST_COMPLETED = 'COMMENT_REACTION_LIST_COMPLETED';
export const COMMENT_REACTION_LIST_FAILED = 'COMMENT_REACTION_LIST_FAILED';
export const COMMENT_REACT_STARTED = 'COMMENT_REACT_STARTED';
export const COMMENT_REACT_COMPLETED = 'COMMENT_REACT_COMPLETED';
export const COMMENT_REACT_FAILED = 'COMMENT_REACT_FAILED';
// Blocked channels
export const TOGGLE_BLOCK_CHANNEL = 'TOGGLE_BLOCK_CHANNEL';

View file

@ -1,7 +1,9 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import * as REACTION_TYPES from 'constants/reactions';
import { Lbry, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
import { doToast } from 'redux/actions/notifications';
import { makeSelectCommentIdsForUri, makeSelectMyReactionsForComment } from 'redux/selectors/comments';
export function doCommentList(uri: string, page: number = 1, pageSize: number = 99999) {
return (dispatch: Dispatch, getState: GetState) => {
@ -39,6 +41,102 @@ export function doCommentList(uri: string, page: number = 1, pageSize: number =
};
}
export function doCommentReactList(uri: string | null, commentId?: string) {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const channel = localStorage.getItem('comment-channel');
// if not channel, fail?
if (!channel) {
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
data: 'No active channel found',
});
return;
}
const commentIds = uri ? makeSelectCommentIdsForUri(uri)(state) : [commentId];
const myChannels = selectMyChannelClaims(state);
const claimForChannelName = myChannels.find(chan => chan.name === channel);
const channelId = claimForChannelName && claimForChannelName.claim_id;
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
});
Lbry.comment_react_list({
comment_ids: commentIds.join(','),
channel_name: channel,
channel_id: channelId,
})
.then((result: CommentReactListResponse) => {
const { my_reactions: myReactions, others_reactions: othersReactions } = result;
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
data: {
myReactions,
othersReactions,
},
});
})
.catch(error => {
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
data: error,
});
});
};
}
export function doCommentReact(commentId: string, type: string) {
return (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const channel = localStorage.getItem('comment-channel');
// if not channel, fail?
if (!channel) {
dispatch({
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
data: 'No active channel found',
});
return;
}
const myChannels = selectMyChannelClaims(state);
const myReacts = makeSelectMyReactionsForComment(commentId)(state);
const claimForChannelName = myChannels.find(chan => chan.name === channel);
const channelId = claimForChannelName && claimForChannelName.claim_id;
const exclusiveTypes = {
[REACTION_TYPES.LIKE]: REACTION_TYPES.DISLIKE,
[REACTION_TYPES.DISLIKE]: REACTION_TYPES.LIKE,
};
dispatch({
type: ACTIONS.COMMENT_REACT_STARTED,
});
const params: CommentReactParams = {
comment_ids: commentId,
channel_name: channel,
channel_id: channelId,
react_type: type,
};
if (Object.keys(exclusiveTypes).includes(type)) {
params['clear_types'] = exclusiveTypes[type];
}
if (myReacts.includes(type)) {
params['remove'] = true;
}
Lbry.comment_react(params)
.then((result: CommentReactListResponse) => {
dispatch({
type: ACTIONS.COMMENT_REACT_COMPLETED,
});
dispatch(doCommentReactList(null, commentId));
})
.catch(error => {
dispatch({
type: ACTIONS.COMMENT_REACT_FAILED,
data: error,
});
});
};
}
export function doCommentCreate(
comment: string = '',
claim_id: string = '',
@ -83,7 +181,6 @@ export function doCommentCreate(
uri,
comment: result,
claimId: claim_id,
uri,
},
});
})

View file

@ -11,6 +11,9 @@ const defaultState: CommentsState = {
isLoading: false,
isCommenting: false,
myComments: undefined,
isFetchingReacts: false,
myReactsByCommentId: {},
othersReactsByCommentId: {},
};
export default handleActions(
@ -56,7 +59,6 @@ export default handleActions(
topLevelCommentsById[claimId].unshift(comment.comment_id);
}
}
return {
...state,
topLevelCommentsById,
@ -69,6 +71,44 @@ export default handleActions(
};
},
[ACTIONS.COMMENT_REACTION_LIST_STARTED]: (state: CommentsState, action: any): CommentsState => ({
...state,
isFetchingReacts: true,
}),
[ACTIONS.COMMENT_REACTION_LIST_FAILED]: (state: CommentsState, action: any) => ({
...state,
isFetchingReacts: false,
}),
[ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
const { myReactions, othersReactions } = action.data;
const myReacts = Object.assign({}, state.myReactsByCommentId);
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
if (myReactions) {
Object.entries(myReactions).forEach(e => {
myReacts[e[0]] = Object.entries(e[1]).reduce((acc, el) => {
if (el[1] === 1) {
acc.push(el[0]);
}
return acc;
}, []);
});
}
if (othersReactions) {
Object.entries(othersReactions).forEach(e => {
othersReacts[e[0]] = e[1];
});
}
return {
...state,
isFetchingReacts: false,
myReactsByCommentId: myReacts,
othersReactsByCommentId: othersReacts,
};
},
[ACTIONS.COMMENT_LIST_STARTED]: state => ({ ...state, isLoading: true }),
[ACTIONS.COMMENT_LIST_COMPLETED]: (state: CommentsState, action: any) => {

View file

@ -88,6 +88,22 @@ export const selectCommentsByUri = createSelector(selectState, state => {
return comments;
});
export const makeSelectCommentIdsForUri = (uri: string) =>
createSelector(selectState, selectCommentsByUri, selectClaimsById, (state, byUri) => {
const claimId = byUri[uri];
return state.byId[claimId];
});
export const makeSelectMyReactionsForComment = (commentId: string) =>
createSelector(selectState, state => {
return state.myReactsByCommentId[commentId];
});
export const makeSelectOthersReactionsForComment = (commentId: string) =>
createSelector(selectState, state => {
return state.othersReactsByCommentId[commentId];
});
export const makeSelectCommentsForUri = (uri: string) =>
createSelector(
selectCommentsByClaimId,

View file

@ -199,7 +199,7 @@ $thumbnailWidthSmall: 1.5rem;
margin-top: var(--spacing-s);
> *:not(:last-child) {
margin-right: var(--spacing-xs);
margin-right: var(--spacing-s);
}
.icon {

View file

@ -6411,9 +6411,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#90012bf47c170f244039261548dab7c7597046dc:
lbry-redux@lbryio/lbry-redux#04015155796bc588bdf5b10762cfc874e6a1b00c:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/90012bf47c170f244039261548dab7c7597046dc"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/04015155796bc588bdf5b10762cfc874e6a1b00c"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"