From 2da7815feb96198a535cec61f2445898ba709a72 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Mon, 9 Aug 2021 14:26:03 +0800 Subject: [PATCH] Livestream: implement Pinned Comments --- flow-typed/Comment.js | 1 + ui/component/livestreamComment/view.jsx | 23 +++++++++++++++- ui/component/livestreamComments/index.js | 2 ++ ui/component/livestreamComments/view.jsx | 24 +++++++++++++++++ ui/redux/actions/websocket.js | 11 ++++++++ ui/redux/reducers/comments.js | 34 +++++++++++++++++++++++- ui/redux/selectors/comments.js | 1 + ui/scss/component/_comments.scss | 3 ++- ui/scss/component/_livestream.scss | 9 +++++++ 9 files changed, 105 insertions(+), 3 deletions(-) diff --git a/flow-typed/Comment.js b/flow-typed/Comment.js index fdfa7880b..17d8eac53 100644 --- a/flow-typed/Comment.js +++ b/flow-typed/Comment.js @@ -38,6 +38,7 @@ declare type CommentsState = { topLevelTotalCommentsById: { [string]: number }, // ClaimID -> total top level comments in commentron. commentById: { [string]: Comment }, linkedCommentAncestors: { [string]: Array }, // {"linkedCommentId": ["parentId", "grandParentId", ...]} + pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs isLoading: boolean, isLoadingByParentId: { [string]: boolean }, myComments: ?Set, diff --git a/ui/component/livestreamComment/view.jsx b/ui/component/livestreamComment/view.jsx index cf3b58b14..a6f603e3f 100644 --- a/ui/component/livestreamComment/view.jsx +++ b/ui/component/livestreamComment/view.jsx @@ -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 && ( + + + {__('Pinned')} + + )}
@@ -78,6 +97,8 @@ function LivestreamComment(props: Props) { authorUri={authorUri} commentIsMine={commentIsMine} disableEdit + isTopLevel + isPinned={isPinned} disableRemove={supportAmount > 0} /> diff --git a/ui/component/livestreamComments/index.js b/ui/component/livestreamComments/index.js index a90e75abe..97cac2e75 100644 --- a/ui/component/livestreamComments/index.js +++ b/ui/component/livestreamComments/index.js @@ -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, { diff --git a/ui/component/livestreamComments/view.jsx b/ui/component/livestreamComments/view.jsx index f4b073fef..e0125c039 100644 --- a/ui/component/livestreamComments/view.jsx +++ b/ui/component/livestreamComments/view.jsx @@ -24,6 +24,7 @@ type Props = { superChats: Array, superChatsTotalAmount: number, myChannels: ?Array, + pinnedCommentsById: { [claimId: string]: Array }, }; 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) {
)} + {pinnedComment && ( +
+ +
+ )} + {!fetchingComments && comments.length > 0 ? (
{commentsToDisplay.map((comment) => ( diff --git a/ui/redux/actions/websocket.js b/ui/redux/actions/websocket.js index 7c5532ea4..78552ab18 100644 --- a/ui/redux/actions/websocket.js +++ b/ui/redux/actions/websocket.js @@ -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, + }, + }); + } }); }; diff --git a/ui/redux/reducers/comments.js b/ui/redux/reducers/comments.js index c81725a1e..4b5af666a 100644 --- a/ui/redux/reducers/comments.js +++ b/ui/redux/reducers/comments.js @@ -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, }; }, diff --git a/ui/redux/selectors/comments.js b/ui/redux/selectors/comments.js index 8d7a898ff..1a2ec9e78 100644 --- a/ui/redux/selectors/comments.js +++ b/ui/redux/selectors/comments.js @@ -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() : [] diff --git a/ui/scss/component/_comments.scss b/ui/scss/component/_comments.scss index e99a2345a..407297db1 100644 --- a/ui/scss/component/_comments.scss +++ b/ui/scss/component/_comments.scss @@ -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; diff --git a/ui/scss/component/_livestream.scss b/ui/scss/component/_livestream.scss index 22fdbe239..ddf744bfa 100644 --- a/ui/scss/component/_livestream.scss +++ b/ui/scss/component/_livestream.scss @@ -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;