List own comments #7171
9 changed files with 323 additions and 2 deletions
|
@ -209,6 +209,9 @@
|
|||
"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%.",
|
||||
"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",
|
||||
"Comment": "Comment",
|
||||
"Comment --[button to submit something]--": "Comment",
|
||||
|
@ -1460,6 +1463,7 @@
|
|||
"Staked LBRY Credits": "Staked LBRY Credits",
|
||||
"1 comment": "1 comment",
|
||||
"%total_comments% comments": "%total_comments% comments",
|
||||
"No comments": "No comments",
|
||||
"Upvote": "Upvote",
|
||||
"Downvote": "Downvote",
|
||||
"You loved this": "You loved this",
|
||||
|
|
|
@ -59,6 +59,7 @@ const ListBlockedPage = lazyImport(() => import('page/listBlocked' /* webpackChu
|
|||
const ListsPage = lazyImport(() => import('page/lists' /* webpackChunkName: "secondary" */));
|
||||
const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* 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 PasswordSetPage = lazyImport(() => import('page/passwordSet' /* 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.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||
<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/:claimId`} exact component={EmbedWrapperPage} />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { selectLanguage } from 'redux/selectors/settings';
|
||||
|
||||
|
@ -9,6 +9,7 @@ const select = (state) => ({
|
|||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
walletEncrypted: selectWalletIsEncrypted(state),
|
||||
user: selectUser(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
language: selectLanguage(state),
|
||||
});
|
||||
|
||||
|
|
|
@ -15,12 +15,13 @@ type Props = {
|
|||
isAuthenticated: boolean,
|
||||
walletEncrypted: boolean,
|
||||
user: User,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
// --- perform ---
|
||||
doWalletStatus: () => void,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// Determine if password is stored.
|
||||
|
@ -92,6 +93,17 @@ export default function SettingAccount(props: Props) {
|
|||
</SettingsRow>
|
||||
)}
|
||||
{/* @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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -52,6 +52,7 @@ export const PAGE_TITLE = {
|
|||
[PAGES.SETTINGS_STRIPE_ACCOUNT]: 'Bank Accounts',
|
||||
[PAGES.SETTINGS_STRIPE_CARD]: 'Payment Methods',
|
||||
[PAGES.SETTINGS_UPDATE_PWD]: 'Update password',
|
||||
[PAGES.SETTINGS_OWN_COMMENTS]: 'Your comments',
|
||||
[PAGES.SWAP]: 'Swap Credits',
|
||||
[PAGES.TAGS_FOLLOWING]: '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_CREATOR = 'settings/creator';
|
||||
exports.SETTINGS_UPDATE_PWD = 'settings/update_password';
|
||||
exports.SETTINGS_OWN_COMMENTS = 'settings/ownComments';
|
||||
exports.SHOW = 'show';
|
||||
exports.ACCOUNT = 'account';
|
||||
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, d) => dispatch(doCommentListOwn(a, b, c, d)),
|
||||
});
|
||||
|
||||
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 { SORT_BY, 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, sortBy: 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, SORT_BY.NEWEST);
|
||||
}
|
||||
}, [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>
|
||||
);
|
||||
}
|
|
@ -487,3 +487,60 @@ $thumbnailWidthSmall: 1rem;
|
|||
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