comment reactions
This commit is contained in:
parent
bdb3d695ee
commit
63ce107cc1
14 changed files with 252 additions and 16 deletions
12
flow-typed/Comment.js
vendored
12
flow-typed/Comment.js
vendored
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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--"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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' });
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue