Livestream: stop pinned comments from appearing as latest (#6888)
## Ticket 6879: Previously pinned livestream comments show as latest ## Issue `comment.List` will always display the pinned comment first, hence the problem when the chat is refreshed. ## Approach Completely split pinned comments from top-level comments in the Reducer, and the let the GUI (e.g. regular comments, livestream comments) decide how they want to display it. For the case of livestream, there is no need to repeat the pinned comments in the regular chat area, since there is a dedicated area on top.
This commit is contained in:
parent
5f55603fb2
commit
4688b4bf58
6 changed files with 101 additions and 74 deletions
|
@ -15,6 +15,7 @@ import {
|
||||||
selectMyReactionsByCommentId,
|
selectMyReactionsByCommentId,
|
||||||
makeSelectCommentIdsForUri,
|
makeSelectCommentIdsForUri,
|
||||||
selectSettingsByChannelId,
|
selectSettingsByChannelId,
|
||||||
|
makeSelectPinnedCommentsForUri,
|
||||||
} 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';
|
||||||
|
@ -25,6 +26,7 @@ const select = (state, props) => {
|
||||||
return {
|
return {
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
||||||
|
pinnedComments: makeSelectPinnedCommentsForUri(props.uri)(state),
|
||||||
topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
||||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||||
|
|
|
@ -28,6 +28,7 @@ function scaleToDevicePixelRatio(value) {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
allCommentIds: any,
|
allCommentIds: any,
|
||||||
|
pinnedComments: Array<Comment>,
|
||||||
topLevelComments: Array<Comment>,
|
topLevelComments: Array<Comment>,
|
||||||
topLevelTotalPages: number,
|
topLevelTotalPages: number,
|
||||||
fetchTopLevelComments: (string, number, number, number) => void,
|
fetchTopLevelComments: (string, number, number, number) => void,
|
||||||
|
@ -57,6 +58,7 @@ function CommentList(props: Props) {
|
||||||
fetchReacts,
|
fetchReacts,
|
||||||
resetComments,
|
resetComments,
|
||||||
uri,
|
uri,
|
||||||
|
pinnedComments,
|
||||||
topLevelComments,
|
topLevelComments,
|
||||||
topLevelTotalPages,
|
topLevelTotalPages,
|
||||||
claim,
|
claim,
|
||||||
|
@ -111,6 +113,34 @@ function CommentList(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCommentElems(comments) {
|
||||||
|
return comments.map((comment) => {
|
||||||
|
return (
|
||||||
|
<CommentView
|
||||||
|
isTopLevel
|
||||||
|
threadDepth={3}
|
||||||
|
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 && isMyComment(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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Reset comments
|
// Reset comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
|
@ -223,8 +253,6 @@ function CommentList(props: Props) {
|
||||||
topLevelTotalPages,
|
topLevelTotalPages,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const displayedComments = readyToDisplayComments ? topLevelComments : [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
|
@ -295,33 +323,8 @@ function CommentList(props: Props) {
|
||||||
})}
|
})}
|
||||||
ref={commentRef}
|
ref={commentRef}
|
||||||
>
|
>
|
||||||
{topLevelComments &&
|
{readyToDisplayComments && pinnedComments && getCommentElems(pinnedComments)}
|
||||||
displayedComments &&
|
{readyToDisplayComments && topLevelComments && getCommentElems(topLevelComments)}
|
||||||
displayedComments.map((comment) => {
|
|
||||||
return (
|
|
||||||
<CommentView
|
|
||||||
isTopLevel
|
|
||||||
threadDepth={3}
|
|
||||||
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 && isMyComment(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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
|
|
|
@ -3,22 +3,22 @@ import { makeSelectClaimForUri, selectMyChannelClaims } from 'lbry-redux';
|
||||||
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
|
||||||
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||||
import {
|
import {
|
||||||
selectPinnedCommentsById,
|
|
||||||
makeSelectTopLevelCommentsForUri,
|
makeSelectTopLevelCommentsForUri,
|
||||||
selectIsFetchingComments,
|
selectIsFetchingComments,
|
||||||
makeSelectSuperChatsForUri,
|
makeSelectSuperChatsForUri,
|
||||||
makeSelectSuperChatTotalAmountForUri,
|
makeSelectSuperChatTotalAmountForUri,
|
||||||
|
makeSelectPinnedCommentsForUri,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import LivestreamComments from './view';
|
import LivestreamComments from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
|
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
|
||||||
|
pinnedComments: makeSelectPinnedCommentsForUri(props.uri)(state),
|
||||||
fetchingComments: selectIsFetchingComments(state),
|
fetchingComments: selectIsFetchingComments(state),
|
||||||
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
||||||
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
pinnedCommentsById: selectPinnedCommentsById(state),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
|
|
|
@ -19,11 +19,11 @@ type Props = {
|
||||||
doCommentSocketDisconnect: (string) => void,
|
doCommentSocketDisconnect: (string) => void,
|
||||||
doCommentList: (string, string, number, number) => void,
|
doCommentList: (string, string, number, number) => void,
|
||||||
comments: Array<Comment>,
|
comments: Array<Comment>,
|
||||||
|
pinnedComments: Array<Comment>,
|
||||||
fetchingComments: boolean,
|
fetchingComments: boolean,
|
||||||
doSuperChatList: (string) => void,
|
doSuperChatList: (string) => void,
|
||||||
superChats: Array<Comment>,
|
superChats: Array<Comment>,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
pinnedCommentsById: { [claimId: string]: Array<string> },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIEW_MODE_CHAT = 'view_chat';
|
const VIEW_MODE_CHAT = 'view_chat';
|
||||||
|
@ -38,12 +38,12 @@ export default function LivestreamComments(props: Props) {
|
||||||
doCommentSocketConnect,
|
doCommentSocketConnect,
|
||||||
doCommentSocketDisconnect,
|
doCommentSocketDisconnect,
|
||||||
comments: commentsByChronologicalOrder,
|
comments: commentsByChronologicalOrder,
|
||||||
|
pinnedComments,
|
||||||
doCommentList,
|
doCommentList,
|
||||||
fetchingComments,
|
fetchingComments,
|
||||||
doSuperChatList,
|
doSuperChatList,
|
||||||
myChannels,
|
myChannels,
|
||||||
superChats: superChatsByTipAmount,
|
superChats: superChatsByTipAmount,
|
||||||
pinnedCommentsById,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
let superChatsFiatAmount, superChatsTotalAmount;
|
let superChatsFiatAmount, superChatsTotalAmount;
|
||||||
|
@ -57,11 +57,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
|
|
||||||
const discussionElement = document.querySelector('.livestream__comments');
|
const discussionElement = document.querySelector('.livestream__comments');
|
||||||
|
|
||||||
let pinnedComment;
|
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
|
||||||
const pinnedCommentIds = (claimId && pinnedCommentsById[claimId]) || [];
|
|
||||||
if (pinnedCommentIds.length > 0) {
|
|
||||||
pinnedComment = commentsByChronologicalOrder.find((c) => c.comment_id === pinnedCommentIds[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreScrollPos() {
|
function restoreScrollPos() {
|
||||||
if (discussionElement) {
|
if (discussionElement) {
|
||||||
|
|
|
@ -301,9 +301,10 @@ export default handleActions(
|
||||||
for (let i = 0; i < comments.length; ++i) {
|
for (let i = 0; i < comments.length; ++i) {
|
||||||
const comment = comments[i];
|
const comment = comments[i];
|
||||||
commonUpdateAction(comment, commentById, commentIds, i);
|
commonUpdateAction(comment, commentById, commentIds, i);
|
||||||
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
|
||||||
if (comment.is_pinned) {
|
if (comment.is_pinned) {
|
||||||
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id);
|
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id);
|
||||||
|
} else {
|
||||||
|
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -615,45 +616,50 @@ export default handleActions(
|
||||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
||||||
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
|
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
|
||||||
|
|
||||||
if (pinnedComment && topLevelCommentsById[claimId]) {
|
if (pinnedComment) {
|
||||||
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
if (topLevelCommentsById[claimId]) {
|
||||||
if (index > -1) {
|
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||||
topLevelCommentsById[claimId].splice(index, 1);
|
if (index > -1) {
|
||||||
|
topLevelCommentsById[claimId].splice(index, 1);
|
||||||
if (pinnedCommentsById[claimId]) {
|
|
||||||
// Remove here so that the 'unshift' below will be a unique entry.
|
|
||||||
pinnedCommentsById[claimId] = pinnedCommentsById[claimId].filter((x) => x !== pinnedComment.comment_id);
|
|
||||||
} else {
|
|
||||||
pinnedCommentsById[claimId] = [];
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
topLevelCommentsById[claimId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (unpin) {
|
if (pinnedCommentsById[claimId]) {
|
||||||
// Without the sort score, I have no idea where to put it. Just
|
const index = pinnedCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||||
// dump it at the bottom. Users can refresh if they want it back to
|
if (index > -1) {
|
||||||
// the correct sorted position.
|
pinnedCommentsById[claimId].splice(index, 1);
|
||||||
topLevelCommentsById[claimId].push(pinnedComment.comment_id);
|
|
||||||
} else {
|
|
||||||
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
|
|
||||||
pinnedCommentsById[claimId].unshift(pinnedComment.comment_id);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
pinnedCommentsById[claimId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (commentById[pinnedComment.comment_id]) {
|
if (unpin) {
|
||||||
// Commentron's `comment.Pin` response places the creator's credentials
|
// Without the sort score, I have no idea where to put it. Just
|
||||||
// in the 'channel_*' fields, which doesn't make sense. Maybe it is to
|
// dump it at the top. Users can refresh if they want it back to
|
||||||
// show who signed/pinned it, but even if so, it shouldn't overload
|
// the correct sorted position.
|
||||||
// these variables which are already used by existing comment data structure.
|
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||||
// Ensure we don't override the existing/correct values, but fallback
|
} else {
|
||||||
// to whatever was given.
|
pinnedCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||||
const { channel_id, channel_name, channel_url } = commentById[pinnedComment.comment_id];
|
}
|
||||||
commentById[pinnedComment.comment_id] = {
|
|
||||||
...pinnedComment,
|
if (commentById[pinnedComment.comment_id]) {
|
||||||
channel_id: channel_id || pinnedComment.channel_id,
|
// Commentron's `comment.Pin` response places the creator's credentials
|
||||||
channel_name: channel_name || pinnedComment.channel_name,
|
// in the 'channel_*' fields, which doesn't make sense. Maybe it is to
|
||||||
channel_url: channel_url || pinnedComment.channel_url,
|
// show who signed/pinned it, but even if so, it shouldn't overload
|
||||||
};
|
// these variables which are already used by existing comment data structure.
|
||||||
} else {
|
// Ensure we don't override the existing/correct values, but fallback
|
||||||
commentById[pinnedComment.comment_id] = pinnedComment;
|
// to whatever was given.
|
||||||
}
|
const { channel_id, channel_name, channel_url } = commentById[pinnedComment.comment_id];
|
||||||
|
commentById[pinnedComment.comment_id] = {
|
||||||
|
...pinnedComment,
|
||||||
|
channel_id: channel_id || pinnedComment.channel_id,
|
||||||
|
channel_name: channel_name || pinnedComment.channel_name,
|
||||||
|
channel_url: channel_url || pinnedComment.channel_url,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
commentById[pinnedComment.comment_id] = pinnedComment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,27 @@ export const selectIsFetchingCommentsByParentId = createSelector(selectState, (s
|
||||||
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
||||||
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
||||||
export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
|
export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
|
||||||
|
|
||||||
export const selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById);
|
export const selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById);
|
||||||
|
export const makeSelectPinnedCommentsForUri = (uri: string) =>
|
||||||
|
createSelector(
|
||||||
|
selectCommentsByUri,
|
||||||
|
selectCommentsById,
|
||||||
|
selectPinnedCommentsById,
|
||||||
|
(byUri, byId, pinnedCommentsById) => {
|
||||||
|
const claimId = byUri[uri];
|
||||||
|
const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId];
|
||||||
|
const pinnedComments = [];
|
||||||
|
|
||||||
|
if (pinnedCommentIds) {
|
||||||
|
pinnedCommentIds.forEach((commentId) => {
|
||||||
|
pinnedComments.push(byId[commentId]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pinnedComments;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const selectModerationBlockList = createSelector(selectState, (state) =>
|
export const selectModerationBlockList = createSelector(selectState, (state) =>
|
||||||
state.moderationBlockList ? state.moderationBlockList.reverse() : []
|
state.moderationBlockList ? state.moderationBlockList.reverse() : []
|
||||||
|
|
Loading…
Reference in a new issue