List own comments (#7171)
* Add option to pass in url-search params. Impetus: allow linked comment ID and setting the discussion tab when clicking on the `ClaimPreview`. * comment.list: fix typos and renamed variables - Switch from 'author' to 'creator' to disambiguate between comment author and content author. For comment author, we'll use 'commenter' from now on. - Corrected 'commenterClaimId' to 'creatorClaimId' (just a typo, no functional change). * doCommentReset: change param from uri to claimId This reduces one lookup as clients will always have the claimID ready, but might not have the full URI. It was using URI previously just to match the other APIs. * Add doCommentListOwn -- command to fetch own comments Since the redux slice is set up based on content or channel ID (for Channel Discussion page), re-use the channel ID for the case of "own comments". We always clear each ID when fetching page-0, so no worries of conflict when actually browsing the Channel Discussion page. * Comment: add option to hide the actions section * Implement own-comments page * Use new param to remove sort-pins-first. comment.List currently always pushes pins to the top to support pagination. This new param removes this behavior.
This commit is contained in:
parent
d3be8726fc
commit
de6c6f9bfd
17 changed files with 472 additions and 39 deletions
4
flow-typed/Comment.js
vendored
4
flow-typed/Comment.js
vendored
|
@ -111,14 +111,14 @@ declare type ReactionListResponse = {
|
||||||
declare type CommentListParams = {
|
declare type CommentListParams = {
|
||||||
page: number, // pagination: which page of results
|
page: number, // pagination: which page of results
|
||||||
page_size: number, // pagination: nr of comments to show in a page (max 200)
|
page_size: number, // pagination: nr of comments to show in a page (max 200)
|
||||||
claim_id: string, // claim id of claim being commented on
|
claim_id?: string, // claim id of claim being commented on
|
||||||
channel_name?: string, // signing channel name of claim (enables 'commentsEnabled' check)
|
channel_name?: string, // signing channel name of claim (enables 'commentsEnabled' check)
|
||||||
channel_id?: string, // signing channel claim id of claim (enables 'commentsEnabled' check)
|
channel_id?: string, // signing channel claim id of claim (enables 'commentsEnabled' check)
|
||||||
author_claim_id?: string, // filters comments to just this author
|
author_claim_id?: string, // filters comments to just this author
|
||||||
parent_id?: string, // filters comments to those under this thread
|
parent_id?: string, // filters comments to those under this thread
|
||||||
top_level?: boolean, // filters to only top level comments
|
top_level?: boolean, // filters to only top level comments
|
||||||
hidden?: boolean, // if true, will show hidden comments as well
|
hidden?: boolean, // if true, will show hidden comments as well
|
||||||
sort_by?: number, // NEWEST=0, OLDEST=1, CONTROVERSY=2, POPULARITY=3,
|
sort_by?: number, // @see: ui/constants/comments.js::SORT_BY
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type CommentListResponse = {
|
declare type CommentListResponse = {
|
||||||
|
|
|
@ -209,6 +209,9 @@
|
||||||
"Failed to copy.": "Failed to copy.",
|
"Failed to copy.": "Failed to copy.",
|
||||||
"The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.": "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.",
|
"The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.": "The publisher has chosen to charge %lbc% to view this content. Your balance is currently too low to view it. Check out %reward_link% for free %lbc% or send more %lbc% to your wallet. You can also %buy_link% more %lbc%.",
|
||||||
"Connecting...": "Connecting...",
|
"Connecting...": "Connecting...",
|
||||||
|
"Your comments": "Your comments",
|
||||||
|
"View your past comments.": "View your past comments.",
|
||||||
|
"Content or channel was deleted.": "Content or channel was deleted.",
|
||||||
"Comments": "Comments",
|
"Comments": "Comments",
|
||||||
"Comment": "Comment",
|
"Comment": "Comment",
|
||||||
"Comment --[button to submit something]--": "Comment",
|
"Comment --[button to submit something]--": "Comment",
|
||||||
|
@ -1460,6 +1463,7 @@
|
||||||
"Staked LBRY Credits": "Staked LBRY Credits",
|
"Staked LBRY Credits": "Staked LBRY Credits",
|
||||||
"1 comment": "1 comment",
|
"1 comment": "1 comment",
|
||||||
"%total_comments% comments": "%total_comments% comments",
|
"%total_comments% comments": "%total_comments% comments",
|
||||||
|
"No comments": "No comments",
|
||||||
"Upvote": "Upvote",
|
"Upvote": "Upvote",
|
||||||
"Downvote": "Downvote",
|
"Downvote": "Downvote",
|
||||||
"You loved this": "You loved this",
|
"You loved this": "You loved this",
|
||||||
|
|
|
@ -70,6 +70,7 @@ type Props = {
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
getFile: (string) => void,
|
getFile: (string) => void,
|
||||||
customShouldHide?: (Claim) => boolean,
|
customShouldHide?: (Claim) => boolean,
|
||||||
|
searchParams?: { [string]: string },
|
||||||
showUnresolvedClaim?: boolean,
|
showUnresolvedClaim?: boolean,
|
||||||
showNullPlaceholder?: boolean,
|
showNullPlaceholder?: boolean,
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
|
@ -125,6 +126,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
// modifiers
|
// modifiers
|
||||||
active,
|
active,
|
||||||
customShouldHide,
|
customShouldHide,
|
||||||
|
searchParams,
|
||||||
showNullPlaceholder,
|
showNullPlaceholder,
|
||||||
// value from show mature content user setting
|
// value from show mature content user setting
|
||||||
// true if the user doesn't wanna see nsfw content
|
// true if the user doesn't wanna see nsfw content
|
||||||
|
@ -221,6 +223,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
if (listId) {
|
if (listId) {
|
||||||
navigateSearch.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
|
navigateSearch.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
|
||||||
}
|
}
|
||||||
|
if (searchParams) {
|
||||||
|
Object.keys(searchParams).forEach((key) => {
|
||||||
|
navigateSearch.set(key, searchParams[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleNavLinkClick = (e) => {
|
const handleNavLinkClick = (e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
|
|
@ -52,6 +52,7 @@ type Props = {
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
isTopLevel?: boolean,
|
isTopLevel?: boolean,
|
||||||
threadDepth: number,
|
threadDepth: number,
|
||||||
|
hideActions?: boolean,
|
||||||
isPinned: boolean,
|
isPinned: boolean,
|
||||||
othersReacts: ?{
|
othersReacts: ?{
|
||||||
like: number,
|
like: number,
|
||||||
|
@ -95,6 +96,7 @@ function Comment(props: Props) {
|
||||||
doToast,
|
doToast,
|
||||||
isTopLevel,
|
isTopLevel,
|
||||||
threadDepth,
|
threadDepth,
|
||||||
|
hideActions,
|
||||||
isPinned,
|
isPinned,
|
||||||
othersReacts,
|
othersReacts,
|
||||||
playingUri,
|
playingUri,
|
||||||
|
@ -348,6 +350,7 @@ function Comment(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!hideActions && (
|
||||||
<div className="comment__actions">
|
<div className="comment__actions">
|
||||||
{threadDepth !== 0 && (
|
{threadDepth !== 0 && (
|
||||||
<Button
|
<Button
|
||||||
|
@ -360,6 +363,7 @@ function Comment(props: Props) {
|
||||||
)}
|
)}
|
||||||
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
{ENABLE_COMMENT_REACTIONS && <CommentReactions uri={uri} commentId={commentId} />}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{numDirectReplies > 0 && !showReplies && (
|
{numDirectReplies > 0 && !showReplies && (
|
||||||
<div className="comment__actions">
|
<div className="comment__actions">
|
||||||
|
|
|
@ -48,7 +48,7 @@ const perform = (dispatch) => ({
|
||||||
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
fetchTopLevelComments: (uri, page, pageSize, sortBy) => dispatch(doCommentList(uri, '', page, pageSize, sortBy)),
|
||||||
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
fetchComment: (commentId) => dispatch(doCommentById(commentId)),
|
||||||
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
fetchReacts: (commentIds) => dispatch(doCommentReactList(commentIds)),
|
||||||
resetComments: (uri) => dispatch(doCommentReset(uri)),
|
resetComments: (claimId) => dispatch(doCommentReset(claimId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -151,10 +151,13 @@ function CommentList(props: Props) {
|
||||||
// Reset comments
|
// Reset comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page === 0) {
|
if (page === 0) {
|
||||||
resetComments(uri);
|
if (claim) {
|
||||||
|
resetComments(claim.claim_id);
|
||||||
|
}
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
}, [page, uri, resetComments]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [page, uri, resetComments]); // 'claim' is derived from 'uri'
|
||||||
|
|
||||||
// Fetch top-level comments
|
// Fetch top-level comments
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -59,6 +59,7 @@ const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChu
|
||||||
const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "secondary" */));
|
const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "secondary" */));
|
||||||
const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "secondary" */));
|
const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* webpackChunkName: "secondary" */));
|
||||||
const LivestreamCurrentPage = lazyImport(() => import('page/livestreamCurrent' /* webpackChunkName: "secondary" */));
|
const LivestreamCurrentPage = lazyImport(() => import('page/livestreamCurrent' /* webpackChunkName: "secondary" */));
|
||||||
|
const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */));
|
||||||
const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "secondary" */));
|
const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "secondary" */));
|
||||||
const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "secondary" */));
|
const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "secondary" */));
|
||||||
const PublishPage = lazyImport(() => import('page/publish' /* webpackChunkName: "secondary" */));
|
const PublishPage = lazyImport(() => import('page/publish' /* webpackChunkName: "secondary" */));
|
||||||
|
@ -329,6 +330,7 @@ function AppRouter(props: Props) {
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SWAP}`} component={SwapPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SWAP}`} component={SwapPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
||||||
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`} component={OwnComments} />
|
||||||
|
|
||||||
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
|
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
|
||||||
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
|
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux';
|
import { doWalletStatus, selectMyChannelClaims, selectWalletIsEncrypted } from 'lbry-redux';
|
||||||
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUser, selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectLanguage } from 'redux/selectors/settings';
|
import { selectLanguage } from 'redux/selectors/settings';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ const select = (state) => ({
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
walletEncrypted: selectWalletIsEncrypted(state),
|
walletEncrypted: selectWalletIsEncrypted(state),
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
|
myChannels: selectMyChannelClaims(state),
|
||||||
language: selectLanguage(state),
|
language: selectLanguage(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,12 +15,13 @@ type Props = {
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
walletEncrypted: boolean,
|
walletEncrypted: boolean,
|
||||||
user: User,
|
user: User,
|
||||||
|
myChannels: ?Array<ChannelClaim>,
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
doWalletStatus: () => void,
|
doWalletStatus: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingAccount(props: Props) {
|
export default function SettingAccount(props: Props) {
|
||||||
const { isAuthenticated, walletEncrypted, user, doWalletStatus } = props;
|
const { isAuthenticated, walletEncrypted, user, myChannels, doWalletStatus } = props;
|
||||||
const [storedPassword, setStoredPassword] = React.useState(false);
|
const [storedPassword, setStoredPassword] = React.useState(false);
|
||||||
|
|
||||||
// Determine if password is stored.
|
// Determine if password is stored.
|
||||||
|
@ -92,6 +93,17 @@ export default function SettingAccount(props: Props) {
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
|
{myChannels && (
|
||||||
|
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
|
||||||
|
<Button
|
||||||
|
button="inverse"
|
||||||
|
label={__('Manage')}
|
||||||
|
icon={ICONS.ARROW_RIGHT}
|
||||||
|
navigate={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`}
|
||||||
|
/>
|
||||||
|
</SettingsRow>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const SORT_BY = {
|
||||||
OLDEST: 1,
|
OLDEST: 1,
|
||||||
CONTROVERSY: 2,
|
CONTROVERSY: 2,
|
||||||
POPULARITY: 3,
|
POPULARITY: 3,
|
||||||
|
NEWEST_NO_PINS: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BLOCK_LEVEL = {
|
export const BLOCK_LEVEL = {
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const PAGE_TITLE = {
|
||||||
[PAGES.SETTINGS_STRIPE_ACCOUNT]: 'Bank Accounts',
|
[PAGES.SETTINGS_STRIPE_ACCOUNT]: 'Bank Accounts',
|
||||||
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
||||||
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
||||||
|
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
|
||||||
[PAGES.SWAP]: 'Swap Credits',
|
[PAGES.SWAP]: 'Swap Credits',
|
||||||
[PAGES.TAGS_FOLLOWING]: 'Tags',
|
[PAGES.TAGS_FOLLOWING]: 'Tags',
|
||||||
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
|
[PAGES.TAGS_FOLLOWING_MANAGE]: 'Manage tags',
|
||||||
|
|
|
@ -46,6 +46,7 @@ exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
||||||
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
|
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
|
||||||
exports.SETTINGS_CREATOR = 'settings/creator';
|
exports.SETTINGS_CREATOR = 'settings/creator';
|
||||||
exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
|
exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
|
||||||
|
exports.SETTINGS_OWN_COMMENTS = 'settings/ownComments';
|
||||||
exports.SHOW = 'show';
|
exports.SHOW = 'show';
|
||||||
exports.ACCOUNT = 'account';
|
exports.ACCOUNT = 'account';
|
||||||
exports.SEARCH = 'search';
|
exports.SEARCH = 'search';
|
||||||
|
|
33
ui/page/ownComments/index.js
Normal file
33
ui/page/ownComments/index.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doCommentListOwn, doCommentReset } from 'redux/actions/comments';
|
||||||
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
import {
|
||||||
|
selectIsFetchingComments,
|
||||||
|
makeSelectCommentsForUri,
|
||||||
|
makeSelectTotalCommentsCountForUri,
|
||||||
|
makeSelectTopLevelTotalPagesForUri,
|
||||||
|
} from 'redux/selectors/comments';
|
||||||
|
import { selectClaimsById } from 'lbry-redux';
|
||||||
|
|
||||||
|
import OwnComments from './view';
|
||||||
|
|
||||||
|
const select = (state) => {
|
||||||
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
const uri = activeChannelClaim && activeChannelClaim.canonical_url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeChannelClaim,
|
||||||
|
allComments: makeSelectCommentsForUri(uri)(state),
|
||||||
|
totalComments: makeSelectTotalCommentsCountForUri(uri)(state),
|
||||||
|
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(uri)(state),
|
||||||
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
|
claimsById: selectClaimsById(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = (dispatch) => ({
|
||||||
|
doCommentReset: (a) => dispatch(doCommentReset(a)),
|
||||||
|
doCommentListOwn: (a, b, c) => dispatch(doCommentListOwn(a, b, c)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(OwnComments);
|
210
ui/page/ownComments/view.jsx
Normal file
210
ui/page/ownComments/view.jsx
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import ChannelSelector from 'component/channelSelector';
|
||||||
|
import ClaimPreview from 'component/claimPreview';
|
||||||
|
import Comment from 'component/comment';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Empty from 'component/common/empty';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
import { COMMENT_PAGE_SIZE_TOP_LEVEL } from 'constants/comment';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import useFetched from 'effects/use-fetched';
|
||||||
|
import debounce from 'util/debounce';
|
||||||
|
|
||||||
|
function scaleToDevicePixelRatio(value) {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
||||||
|
if (devicePixelRatio < 1.0) {
|
||||||
|
return Math.ceil(value / devicePixelRatio);
|
||||||
|
}
|
||||||
|
return Math.ceil(value * devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeChannelClaim: ?ChannelClaim,
|
||||||
|
allComments: Array<Comment>,
|
||||||
|
totalComments: number,
|
||||||
|
topLevelTotalPages: number,
|
||||||
|
isFetchingComments: boolean,
|
||||||
|
claimsById: any,
|
||||||
|
doCommentReset: (claimId: string) => void,
|
||||||
|
doCommentListOwn: (channelId: string, page: number, pageSize: number) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OwnComments(props: Props) {
|
||||||
|
const {
|
||||||
|
activeChannelClaim,
|
||||||
|
allComments,
|
||||||
|
totalComments,
|
||||||
|
isFetchingComments,
|
||||||
|
claimsById,
|
||||||
|
doCommentReset,
|
||||||
|
doCommentListOwn,
|
||||||
|
} = props;
|
||||||
|
const spinnerRef = React.useRef();
|
||||||
|
const [page, setPage] = React.useState(0);
|
||||||
|
const [activeChannelId, setActiveChannelId] = React.useState('');
|
||||||
|
|
||||||
|
// Since we are sharing the key for Discussion and MyComments, don't show
|
||||||
|
// the list until we've gone through the initial reset.
|
||||||
|
const wasResetAndReady = useFetched(isFetchingComments);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(totalComments / COMMENT_PAGE_SIZE_TOP_LEVEL);
|
||||||
|
const moreBelow = page < totalPages;
|
||||||
|
|
||||||
|
function getCommentsElem(comments) {
|
||||||
|
return comments.map((comment) => {
|
||||||
|
const contentClaim = claimsById[comment.claim_id];
|
||||||
|
const isChannel = contentClaim && contentClaim.value_type === 'channel';
|
||||||
|
const isLivestream = Boolean(contentClaim && contentClaim.value_type === 'stream' && !contentClaim.value.source);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={comment.comment_id} className="comments-own card__main-actions">
|
||||||
|
<div className="section__actions">
|
||||||
|
<div className="comments-own--claim">
|
||||||
|
{contentClaim && (
|
||||||
|
<ClaimPreview
|
||||||
|
uri={contentClaim.canonical_url}
|
||||||
|
searchParams={{
|
||||||
|
...(isChannel ? { view: 'discussion' } : {}),
|
||||||
|
...(isLivestream ? {} : { lc: comment.comment_id }),
|
||||||
|
}}
|
||||||
|
hideActions
|
||||||
|
hideMenu
|
||||||
|
properties={() => null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!contentClaim && <Empty text={__('Content or channel was deleted.')} />}
|
||||||
|
</div>
|
||||||
|
<Comment
|
||||||
|
isTopLevel
|
||||||
|
hideActions
|
||||||
|
authorUri={comment.channel_url}
|
||||||
|
author={comment.channel_name}
|
||||||
|
commentId={comment.comment_id}
|
||||||
|
message={comment.comment}
|
||||||
|
timePosted={comment.timestamp * 1000}
|
||||||
|
commentIsMine
|
||||||
|
supportAmount={comment.support_amount}
|
||||||
|
numDirectReplies={0} // Don't show replies here
|
||||||
|
isModerator={comment.is_moderator}
|
||||||
|
isGlobalMod={comment.is_global_mod}
|
||||||
|
isFiat={comment.is_fiat}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active channel changed
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (activeChannelClaim && activeChannelClaim.claim_id !== activeChannelId) {
|
||||||
|
setActiveChannelId(activeChannelClaim.claim_id);
|
||||||
|
setPage(0);
|
||||||
|
}
|
||||||
|
}, [activeChannelClaim, activeChannelId]);
|
||||||
|
|
||||||
|
// Reset comments
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (page === 0 && activeChannelId) {
|
||||||
|
doCommentReset(activeChannelId);
|
||||||
|
setPage(1);
|
||||||
|
}
|
||||||
|
}, [page, activeChannelId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Fetch own comments
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (page !== 0 && activeChannelId) {
|
||||||
|
doCommentListOwn(activeChannelId, page, COMMENT_PAGE_SIZE_TOP_LEVEL);
|
||||||
|
}
|
||||||
|
}, [page]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Infinite scroll
|
||||||
|
React.useEffect(() => {
|
||||||
|
function shouldFetchNextPage(page, topLevelTotalPages, window, document, yPrefetchPx = 1000) {
|
||||||
|
if (!spinnerRef || !spinnerRef.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = spinnerRef.current.getBoundingClientRect(); // $FlowFixMe
|
||||||
|
const windowH = window.innerHeight || document.documentElement.clientHeight; // $FlowFixMe
|
||||||
|
const windowW = window.innerWidth || document.documentElement.clientWidth; // $FlowFixMe
|
||||||
|
|
||||||
|
const isApproachingViewport = yPrefetchPx !== 0 && rect.top < windowH + scaleToDevicePixelRatio(yPrefetchPx);
|
||||||
|
|
||||||
|
const isInViewport =
|
||||||
|
rect.width > 0 &&
|
||||||
|
rect.height > 0 &&
|
||||||
|
rect.bottom >= 0 &&
|
||||||
|
rect.right >= 0 &&
|
||||||
|
// $FlowFixMe
|
||||||
|
rect.top <= windowH &&
|
||||||
|
// $FlowFixMe
|
||||||
|
rect.left <= windowW;
|
||||||
|
|
||||||
|
return (isInViewport || isApproachingViewport) && page < topLevelTotalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommentScroll = debounce(() => {
|
||||||
|
if (shouldFetchNextPage(page, totalPages, window, document)) {
|
||||||
|
setPage(page + 1);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
if (!isFetchingComments && moreBelow && spinnerRef && spinnerRef.current) {
|
||||||
|
if (shouldFetchNextPage(page, totalPages, window, document, 0)) {
|
||||||
|
setPage(page + 1);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('scroll', handleCommentScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleCommentScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [page, spinnerRef, isFetchingComments, moreBelow, totalPages]);
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
if (!activeChannelClaim) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page noFooter noSideNavigation settingsPage backout={{ title: __('Your comments'), backLabel: __('Back') }}>
|
||||||
|
<ChannelSelector hideAnon />
|
||||||
|
<Card
|
||||||
|
isBodyList
|
||||||
|
title={
|
||||||
|
totalComments > 0
|
||||||
|
? totalComments === 1
|
||||||
|
? __('1 comment')
|
||||||
|
: __('%total_comments% comments', { total_comments: totalComments })
|
||||||
|
: isFetchingComments
|
||||||
|
? ''
|
||||||
|
: __('No comments')
|
||||||
|
}
|
||||||
|
titleActions={
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
icon={ICONS.REFRESH}
|
||||||
|
title={__('Refresh')}
|
||||||
|
onClick={() => {
|
||||||
|
setPage(0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
{wasResetAndReady && <ul className="comments">{allComments && getCommentsElem(allComments)}</ul>}
|
||||||
|
{(isFetchingComments || moreBelow) && (
|
||||||
|
<div className="main--empty" ref={spinnerRef}>
|
||||||
|
<Spinner type="small" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,7 +3,15 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
|
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
|
||||||
import { Lbry, parseURI, buildURI, selectClaimsByUri, selectMyChannelClaims, isURIEqual } from 'lbry-redux';
|
import {
|
||||||
|
Lbry,
|
||||||
|
parseURI,
|
||||||
|
buildURI,
|
||||||
|
selectClaimsByUri,
|
||||||
|
selectMyChannelClaims,
|
||||||
|
isURIEqual,
|
||||||
|
doClaimSearch,
|
||||||
|
} from 'lbry-redux';
|
||||||
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
||||||
import {
|
import {
|
||||||
makeSelectMyReactionsForComment,
|
makeSelectMyReactionsForComment,
|
||||||
|
@ -127,7 +135,6 @@ export function doCommentList(
|
||||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||||
data: 'unable to find claim for uri',
|
data: 'unable to find claim for uri',
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +146,7 @@ export function doCommentList(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
|
// Adding 'channel_id' and 'channel_name' enables "CreatorSettings > commentsEnabled".
|
||||||
const authorChannelClaim = claim.value_type === 'channel' ? claim : claim.signing_channel;
|
const creatorChannelClaim = claim.value_type === 'channel' ? claim : claim.signing_channel;
|
||||||
|
|
||||||
return Comments.comment_list({
|
return Comments.comment_list({
|
||||||
page,
|
page,
|
||||||
|
@ -147,8 +154,8 @@ export function doCommentList(
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
parent_id: parentId || undefined,
|
parent_id: parentId || undefined,
|
||||||
top_level: !parentId,
|
top_level: !parentId,
|
||||||
channel_id: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
channel_id: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
|
||||||
channel_name: authorChannelClaim ? authorChannelClaim.name : undefined,
|
channel_name: creatorChannelClaim ? creatorChannelClaim.name : undefined,
|
||||||
sort_by: sortBy,
|
sort_by: sortBy,
|
||||||
})
|
})
|
||||||
.then((result: CommentListResponse) => {
|
.then((result: CommentListResponse) => {
|
||||||
|
@ -162,7 +169,7 @@ export function doCommentList(
|
||||||
totalFilteredItems: total_filtered_items,
|
totalFilteredItems: total_filtered_items,
|
||||||
totalPages: total_pages,
|
totalPages: total_pages,
|
||||||
claimId: claimId,
|
claimId: claimId,
|
||||||
commenterClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
creatorClaimId: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
|
||||||
uri: uri,
|
uri: uri,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -175,7 +182,7 @@ export function doCommentList(
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
||||||
data: {
|
data: {
|
||||||
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
creatorClaimId: creatorChannelClaim ? creatorChannelClaim.claim_id : undefined,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -201,6 +208,103 @@ export function doCommentList(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doCommentListOwn(
|
||||||
|
channelId: string,
|
||||||
|
page: number = 1,
|
||||||
|
pageSize: number = 99999,
|
||||||
|
sortBy: number = SORT_BY.NEWEST_NO_PINS
|
||||||
|
) {
|
||||||
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const myChannelClaims = selectMyChannelClaims(state);
|
||||||
|
if (!myChannelClaims) {
|
||||||
|
console.error('Failed to fetch channel list.'); // eslint-disable-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelClaim = myChannelClaims.find((x) => x.claim_id === channelId);
|
||||||
|
if (!channelClaim) {
|
||||||
|
console.error('You do not own this channel.'); // eslint-disable-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelSignature = await channelSignName(channelClaim.claim_id, channelClaim.name);
|
||||||
|
if (!channelSignature) {
|
||||||
|
console.error('Failed to sign channel name.'); // eslint-disable-line
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_LIST_STARTED,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Comments.comment_list({
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
sort_by: sortBy,
|
||||||
|
author_claim_id: channelId,
|
||||||
|
requestor_channel_name: channelClaim.name,
|
||||||
|
requestor_channel_id: channelClaim.claim_id,
|
||||||
|
signature: channelSignature.signature,
|
||||||
|
signing_ts: channelSignature.signing_ts,
|
||||||
|
})
|
||||||
|
.then((result: CommentListResponse) => {
|
||||||
|
const { items: comments, total_items, total_filtered_items, total_pages } = result;
|
||||||
|
|
||||||
|
if (!comments) {
|
||||||
|
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: 'No more comments.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
doClaimSearch({
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
no_totals: true,
|
||||||
|
claim_ids: comments.map((c) => c.claim_id),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
||||||
|
data: {
|
||||||
|
comments,
|
||||||
|
totalItems: total_items,
|
||||||
|
totalFilteredItems: total_filtered_items,
|
||||||
|
totalPages: total_pages,
|
||||||
|
uri: channelClaim.canonical_url, // hijack "Discussion Page"
|
||||||
|
claimId: channelClaim.claim_id, // hijack "Discussion Page"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: err });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
switch (error.message) {
|
||||||
|
case FETCH_API_FAILED_TO_FETCH:
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
isError: true,
|
||||||
|
message: Comments.isCustomServer
|
||||||
|
? __('Failed to fetch comments. Verify custom server settings.')
|
||||||
|
: __('Failed to fetch comments.'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(doToast({ isError: true, message: `${error.message}` }));
|
||||||
|
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
dispatch(doToast({ isError: true, message: `${error.message}` }));
|
||||||
|
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function doCommentById(commentId: string, toastIfNotFound: boolean = true) {
|
export function doCommentById(commentId: string, toastIfNotFound: boolean = true) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -239,17 +343,10 @@ export function doCommentById(commentId: string, toastIfNotFound: boolean = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCommentReset(uri: string) {
|
export function doCommentReset(claimId: string) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return (dispatch: Dispatch) => {
|
||||||
const state = getState();
|
|
||||||
const claim = selectClaimsByUri(state)[uri];
|
|
||||||
const claimId = claim ? claim.claim_id : null;
|
|
||||||
|
|
||||||
if (!claimId) {
|
if (!claimId) {
|
||||||
dispatch({
|
console.error(`Failed to reset comments`); //eslint-disable-line
|
||||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
|
||||||
data: 'unable to find claim for uri',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -246,7 +246,7 @@ export default handleActions(
|
||||||
claimId,
|
claimId,
|
||||||
uri,
|
uri,
|
||||||
disabled,
|
disabled,
|
||||||
commenterClaimId,
|
creatorClaimId,
|
||||||
} = action.data;
|
} = action.data;
|
||||||
|
|
||||||
const commentById = Object.assign({}, state.commentById);
|
const commentById = Object.assign({}, state.commentById);
|
||||||
|
@ -262,8 +262,8 @@ export default handleActions(
|
||||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||||
const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
|
const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
|
||||||
|
|
||||||
settingsByChannelId[commenterClaimId] = {
|
settingsByChannelId[creatorClaimId] = {
|
||||||
...(settingsByChannelId[commenterClaimId] || {}),
|
...(settingsByChannelId[creatorClaimId] || {}),
|
||||||
comments_enabled: !disabled,
|
comments_enabled: !disabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -487,3 +487,60 @@ $thumbnailWidthSmall: 1rem;
|
||||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comments-own {
|
||||||
|
.section__actions {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comments-own--claim {
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
min-width: 40%;
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media__thumb {
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
$width: 5rem;
|
||||||
|
@include handleClaimListGifThumbnail($width);
|
||||||
|
width: $width;
|
||||||
|
height: calc(#{$width} * (9 / 16));
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-thumbnail {
|
||||||
|
@include handleChannelGif(calc(5rem * 9 / 16));
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
@include handleChannelGif(calc(5rem * 9 / 16));
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-preview__wrapper {
|
||||||
|
margin: 0 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin: 0 var(--spacing-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin-top: var(--spacing-s);
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
border-left: 4px solid var(--color-border);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue