Livestream: implement Pinned Comments
This commit is contained in:
parent
c2b51127ac
commit
4731786a3f
9 changed files with 116 additions and 2 deletions
1
flow-typed/Comment.js
vendored
1
flow-typed/Comment.js
vendored
|
@ -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>,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,16 +648,34 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
commentById[pinnedComment.comment_id] = pinnedComment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
commentById,
|
commentById,
|
||||||
topLevelCommentsById,
|
topLevelCommentsById,
|
||||||
|
pinnedCommentsById,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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() : []
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue