Livestream: implement Pinned Comments

# Conflicts:
#	ui/component/livestreamComment/view.jsx
This commit is contained in:
infinite-persistence 2021-08-09 14:26:03 +08:00
parent 733458214a
commit 6d62ccb21a
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
9 changed files with 94 additions and 2 deletions

View file

@ -41,6 +41,7 @@ declare type CommentsState = {
topLevelTotalCommentsById: { [string]: number }, // ClaimID -> total top level comments in commentron. topLevelTotalCommentsById: { [string]: number }, // ClaimID -> total top level comments in commentron.
commentById: { [string]: Comment }, commentById: { [string]: Comment },
linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]} linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
isLoading: boolean, isLoading: boolean,
isLoadingByParentId: { [string]: boolean }, isLoadingByParentId: { [string]: boolean },
myComments: ?Set<string>, myComments: ?Set<string>,

View file

@ -23,6 +23,7 @@ type Props = {
isModerator: boolean, isModerator: boolean,
isGlobalMod: boolean, isGlobalMod: boolean,
isFiat: boolean, isFiat: boolean,
isPinned: boolean,
}; };
function LivestreamComment(props: Props) { function LivestreamComment(props: Props) {
@ -38,6 +39,7 @@ function LivestreamComment(props: Props) {
isModerator, isModerator,
isGlobalMod, isGlobalMod,
isFiat, isFiat,
isPinned,
} = props; } = props;
const [mouseIsHovering, setMouseHover] = React.useState(false); const [mouseIsHovering, setMouseHover] = React.useState(false);
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri; const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
@ -81,6 +83,13 @@ function LivestreamComment(props: Props) {
navigate={authorUri} navigate={authorUri}
> >
{claimName} {claimName}
{isPinned && (
<span className="comment__pin">
<Icon icon={ICONS.PIN} size={14} />
{__('Pinned')}
</span>
)}
</Button> </Button>
<div className="livestream-comment__text"> <div className="livestream-comment__text">
@ -104,6 +113,8 @@ function LivestreamComment(props: Props) {
authorUri={authorUri} authorUri={authorUri}
commentIsMine={commentIsMine} commentIsMine={commentIsMine}
disableEdit disableEdit
isTopLevel
isPinned={isPinned}
disableRemove={supportAmount > 0} disableRemove={supportAmount > 0}
/> />
</Menu> </Menu>

View file

@ -3,6 +3,7 @@ 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,
@ -17,6 +18,7 @@ const select = (state, props) => ({
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, {

View file

@ -24,6 +24,7 @@ type Props = {
superChats: Array<Comment>, superChats: Array<Comment>,
superChatsTotalAmount: number, superChatsTotalAmount: number,
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
pinnedCommentsById: { [claimId: string]: Array<string> },
}; };
const VIEW_MODE_CHAT = 'view_chat'; const VIEW_MODE_CHAT = 'view_chat';
@ -45,6 +46,7 @@ export default function LivestreamComments(props: Props) {
superChats, superChats,
superChatsTotalAmount, superChatsTotalAmount,
myChannels, myChannels,
pinnedCommentsById,
} = props; } = props;
const commentsRef = React.createRef(); const commentsRef = React.createRef();
@ -58,6 +60,12 @@ export default function LivestreamComments(props: Props) {
const discussionElement = document.querySelector('.livestream__comments'); const discussionElement = document.querySelector('.livestream__comments');
const commentElement = document.querySelector('.livestream-comment'); const commentElement = document.querySelector('.livestream-comment');
let pinnedComment;
const pinnedCommentIds = (claimId && pinnedCommentsById[claimId]) || [];
if (pinnedCommentIds.length > 0) {
pinnedComment = comments.find((c) => c.comment_id === pinnedCommentIds[0]);
}
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine // todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
function isMyComment(channelId: string) { function isMyComment(channelId: string) {
if (myChannels != null && channelId != null) { if (myChannels != null && channelId != null) {
@ -187,6 +195,22 @@ export default function LivestreamComments(props: Props) {
</div> </div>
)} )}
{pinnedComment && (
<div className="livestream-pinned__wrapper">
<LivestreamComment
key={pinnedComment.comment_id}
uri={uri}
authorUri={pinnedComment.channel_url}
commentId={pinnedComment.comment_id}
message={pinnedComment.comment}
supportAmount={pinnedComment.support_amount}
isFiat={pinnedComment.is_fiat}
isPinned={pinnedComment.is_pinned}
commentIsMine={pinnedComment.channel_id && isMyComment(pinnedComment.channel_id)}
/>
</div>
)}
{!fetchingComments && comments.length > 0 ? ( {!fetchingComments && comments.length > 0 ? (
<div className="livestream__comments"> <div className="livestream__comments">
{commentsToDisplay.map((comment) => ( {commentsToDisplay.map((comment) => (

View file

@ -110,6 +110,17 @@ export const doCommentSocketConnect = (uri, claimId) => (dispatch) => {
data: { connected, claimId }, data: { connected, claimId },
}); });
} }
if (response.type === 'pinned') {
const pinnedComment = response.data.comment;
dispatch({
type: ACTIONS.COMMENT_PIN_COMPLETED,
data: {
pinnedComment: pinnedComment,
claimId,
unpin: !pinnedComment.is_pinned,
},
});
}
}); });
}; };

View file

@ -19,6 +19,7 @@ const defaultState: CommentsState = {
commentsByUri: {}, // URI -> claimId commentsByUri: {}, // URI -> claimId
linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]} linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
superChatsByUri: {}, superChatsByUri: {},
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
isLoading: false, isLoading: false,
isLoadingByParentId: {}, isLoadingByParentId: {},
isCommenting: false, isCommenting: false,
@ -285,6 +286,7 @@ export default handleActions(
const commentsByUri = Object.assign({}, state.commentsByUri); const commentsByUri = Object.assign({}, state.commentsByUri);
const repliesByParentId = Object.assign({}, state.repliesByParentId); const repliesByParentId = Object.assign({}, state.repliesByParentId);
const totalCommentsById = Object.assign({}, state.totalCommentsById); const totalCommentsById = Object.assign({}, state.totalCommentsById);
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId); const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId); const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
@ -315,6 +317,9 @@ export default handleActions(
const comment = comments[i]; const comment = comments[i];
commonUpdateAction(comment, commentById, commentIds, i); commonUpdateAction(comment, commentById, commentIds, i);
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id); pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
if (comment.is_pinned) {
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id);
}
} }
} }
// --- Replies --- // --- Replies ---
@ -337,6 +342,7 @@ export default handleActions(
topLevelTotalPagesById, topLevelTotalPagesById,
repliesByParentId, repliesByParentId,
totalCommentsById, totalCommentsById,
pinnedCommentsById,
totalRepliesByParentId, totalRepliesByParentId,
byId, byId,
commentById, commentById,
@ -621,12 +627,20 @@ export default handleActions(
const { pinnedComment, claimId, unpin } = action.data; const { pinnedComment, claimId, unpin } = action.data;
const commentById = Object.assign({}, state.commentById); const commentById = Object.assign({}, state.commentById);
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById); const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
if (pinnedComment && topLevelCommentsById[claimId]) { if (pinnedComment && topLevelCommentsById[claimId]) {
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id); const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
if (index > -1) { if (index > -1) {
topLevelCommentsById[claimId].splice(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] = [];
}
if (unpin) { if (unpin) {
// Without the sort score, I have no idea where to put it. Just // Without the sort score, I have no idea where to put it. Just
// dump it at the bottom. Users can refresh if they want it back to // dump it at the bottom. Users can refresh if they want it back to
@ -634,9 +648,26 @@ export default handleActions(
topLevelCommentsById[claimId].push(pinnedComment.comment_id); topLevelCommentsById[claimId].push(pinnedComment.comment_id);
} else { } else {
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id); topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
pinnedCommentsById[claimId].unshift(pinnedComment.comment_id);
} }
commentById[pinnedComment.comment_id] = pinnedComment; if (commentById[pinnedComment.comment_id]) {
// Commentron's `comment.Pin` response places the creator's credentials
// in the 'channel_*' fields, which doesn't make sense. Maybe it is to
// show who signed/pinned it, but even if so, it shouldn't overload
// these variables which are already used by existing comment data structure.
// Ensure we don't override the existing/correct values, but fallback
// 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;
}
} }
} }
@ -644,6 +675,7 @@ export default handleActions(
...state, ...state,
commentById, commentById,
topLevelCommentsById, topLevelCommentsById,
pinnedCommentsById,
}; };
}, },

View file

@ -17,6 +17,7 @@ export const selectCommentsDisabledChannelIds = createSelector(
(state) => state.commentsDisabledChannelIds (state) => state.commentsDisabledChannelIds
); );
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 selectModerationBlockList = createSelector(selectState, (state) => export const selectModerationBlockList = createSelector(selectState, (state) =>
state.moderationBlockList ? state.moderationBlockList.reverse() : [] state.moderationBlockList ? state.moderationBlockList.reverse() : []

View file

@ -216,7 +216,8 @@ $thumbnailWidthSmall: 1rem;
} }
.comment__pin { .comment__pin {
margin-left: var(--spacing-s); margin-left: var(--spacing-m);
font-style: italic;
.icon { .icon {
padding-top: 1px; padding-top: 1px;

View file

@ -223,6 +223,15 @@ $discussion-header__height: 3rem;
} }
} }
.livestream-pinned__wrapper {
@extend .livestream-superchats__wrapper;
overflow-x: unset;
overflow-y: scroll;
background-color: var(--color-card-background-highlighted);
max-height: 7rem;
width: 100%;
}
.livestream-superchat__amount-large { .livestream-superchat__amount-large {
.credit-amount { .credit-amount {
display: flex; display: flex;