Livestream: implement Pinned Comments

This commit is contained in:
infinite-persistence 2021-08-09 14:26:03 +08:00
parent 9d663b3789
commit 2da7815feb
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
9 changed files with 105 additions and 3 deletions

View file

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

View file

@ -21,10 +21,22 @@ type Props = {
stakedLevel: number,
supportAmount: number,
isFiat: boolean,
isPinned: boolean,
};
function LivestreamComment(props: Props) {
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount, isFiat } = props;
const {
claim,
uri,
authorUri,
message,
commentIsMine,
commentId,
stakedLevel,
supportAmount,
isFiat,
isPinned,
} = props;
const [mouseIsHovering, setMouseHover] = React.useState(false);
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
const { claimName } = parseURI(authorUri);
@ -55,6 +67,13 @@ function LivestreamComment(props: Props) {
navigate={authorUri}
>
{claimName}
{isPinned && (
<span className="comment__pin">
<Icon icon={ICONS.PIN} size={14} />
{__('Pinned')}
</span>
)}
</Button>
<div className="livestream-comment__text">
@ -78,6 +97,8 @@ function LivestreamComment(props: Props) {
authorUri={authorUri}
commentIsMine={commentIsMine}
disableEdit
isTopLevel
isPinned={isPinned}
disableRemove={supportAmount > 0}
/>
</Menu>

View file

@ -3,6 +3,7 @@ import { makeSelectClaimForUri, selectMyChannelClaims } from 'lbry-redux';
import { doCommentSocketConnect, doCommentSocketDisconnect } from 'redux/actions/websocket';
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
import {
selectPinnedCommentsById,
makeSelectTopLevelCommentsForUri,
selectIsFetchingComments,
makeSelectSuperChatsForUri,
@ -17,6 +18,7 @@ const select = (state, props) => ({
superChats: makeSelectSuperChatsForUri(props.uri)(state),
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
myChannels: selectMyChannelClaims(state),
pinnedCommentsById: selectPinnedCommentsById(state),
});
export default connect(select, {

View file

@ -24,6 +24,7 @@ type Props = {
superChats: Array<Comment>,
superChatsTotalAmount: number,
myChannels: ?Array<ChannelClaim>,
pinnedCommentsById: { [claimId: string]: Array<string> },
};
const VIEW_MODE_CHAT = 'view_chat';
@ -45,6 +46,7 @@ export default function LivestreamComments(props: Props) {
superChats,
superChatsTotalAmount,
myChannels,
pinnedCommentsById,
} = props;
const commentsRef = React.createRef();
@ -58,6 +60,12 @@ export default function LivestreamComments(props: Props) {
const discussionElement = document.querySelector('.livestream__comments');
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
function isMyComment(channelId: string) {
if (myChannels != null && channelId != null) {
@ -187,6 +195,22 @@ export default function LivestreamComments(props: Props) {
</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 ? (
<div className="livestream__comments">
{commentsToDisplay.map((comment) => (

View file

@ -110,6 +110,17 @@ export const doCommentSocketConnect = (uri, claimId) => (dispatch) => {
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
linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
superChatsByUri: {},
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
isLoading: false,
isLoadingByParentId: {},
isCommenting: false,
@ -285,6 +286,7 @@ export default handleActions(
const commentsByUri = Object.assign({}, state.commentsByUri);
const repliesByParentId = Object.assign({}, state.repliesByParentId);
const totalCommentsById = Object.assign({}, state.totalCommentsById);
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
@ -315,6 +317,9 @@ export default handleActions(
const comment = comments[i];
commonUpdateAction(comment, commentById, commentIds, i);
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
if (comment.is_pinned) {
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id);
}
}
}
// --- Replies ---
@ -337,6 +342,7 @@ export default handleActions(
topLevelTotalPagesById,
repliesByParentId,
totalCommentsById,
pinnedCommentsById,
totalRepliesByParentId,
byId,
commentById,
@ -621,12 +627,20 @@ export default handleActions(
const { pinnedComment, claimId, unpin } = action.data;
const commentById = Object.assign({}, state.commentById);
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
if (pinnedComment && topLevelCommentsById[claimId]) {
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
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] = [];
}
if (unpin) {
// 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
@ -634,9 +648,26 @@ export default handleActions(
topLevelCommentsById[claimId].push(pinnedComment.comment_id);
} else {
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,
commentById,
topLevelCommentsById,
pinnedCommentsById,
};
},

View file

@ -17,6 +17,7 @@ export const selectCommentsDisabledChannelIds = createSelector(
(state) => state.commentsDisabledChannelIds
);
export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
export const selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById);
export const selectModerationBlockList = createSelector(selectState, (state) =>
state.moderationBlockList ? state.moderationBlockList.reverse() : []

View file

@ -216,7 +216,8 @@ $thumbnailWidthSmall: 1rem;
}
.comment__pin {
margin-left: var(--spacing-s);
margin-left: var(--spacing-m);
font-style: italic;
.icon {
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 {
.credit-amount {
display: flex;