Livestream: implement Pinned Comments

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

View file

@ -38,6 +38,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

@ -21,10 +21,22 @@ type Props = {
stakedLevel: number, stakedLevel: number,
supportAmount: number, supportAmount: number,
isFiat: boolean, isFiat: boolean,
isPinned: boolean,
}; };
function LivestreamComment(props: Props) { 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 [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;
const { claimName } = parseURI(authorUri); const { claimName } = parseURI(authorUri);
@ -57,6 +69,13 @@ function LivestreamComment(props: Props) {
{claimName} {claimName}
</Button> </Button>
{isPinned && (
<span className="comment__pin">
<Icon icon={ICONS.PIN} size={14} />
{__('Pinned')}
</span>
)}
<div className="livestream-comment__text"> <div className="livestream-comment__text">
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} /> <MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} />
</div> </div>
@ -78,6 +97,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

@ -23,6 +23,7 @@ type Props = {
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';
@ -43,6 +44,7 @@ export default function LivestreamComments(props: Props) {
doSuperChatList, doSuperChatList,
myChannels, myChannels,
superChats: superChatsByTipAmount, superChats: superChatsByTipAmount,
pinnedCommentsById,
} = props; } = props;
let superChatsFiatAmount, superChatsTotalAmount; let superChatsFiatAmount, superChatsTotalAmount;
@ -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 = commentsByChronologicalOrder.find((c) => c.comment_id === pinnedCommentIds[0]);
}
React.useEffect(() => { React.useEffect(() => {
if (claimId) { if (claimId) {
doCommentList(uri, '', 1, 75); doCommentList(uri, '', 1, 75);
@ -234,6 +242,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>
)}
{/* top to bottom comment display */} {/* top to bottom comment display */}
{!fetchingComments && commentsByChronologicalOrder.length > 0 ? ( {!fetchingComments && commentsByChronologicalOrder.length > 0 ? (
<div className="livestream__comments"> <div className="livestream__comments">

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

@ -217,6 +217,7 @@ $thumbnailWidthSmall: 1rem;
.comment__pin { .comment__pin {
margin-left: var(--spacing-s); margin-left: var(--spacing-s);
font-size: var(--font-xsmall);
.icon { .icon {
padding-top: 1px; padding-top: 1px;

View file

@ -223,6 +223,27 @@ $discussion-header__height: 3rem;
} }
} }
.livestream-pinned__wrapper {
flex-shrink: 0;
position: relative;
padding: var(--spacing-s) var(--spacing-xs);
border-bottom: 1px solid var(--color-border);
font-size: var(--font-small);
background-color: var(--color-card-background-highlighted);
width: 100%;
.livestream-comment {
padding-top: var(--spacing-xs);
max-height: 6rem;
overflow-y: scroll;
}
@media (min-width: $breakpoint-small) {
padding: var(--spacing-xs);
width: var(--livestream-comments-width);
}
}
.livestream-superchat__amount-large { .livestream-superchat__amount-large {
.credit-amount { .credit-amount {
display: flex; display: flex;