Show content view counts on channel pages
## Issue 3587 Show content view counts on channel pages ## Notes Limited to just "channel pages" for now as specified in the ticket. Can be enabled for all claim previews, as long as there's an efficient spot to run the batch fetching. Either make `fetchViewCount` prop default to true, or add the parameter in places that need it.
This commit is contained in:
parent
89a0d5e597
commit
093c427b83
12 changed files with 125 additions and 16 deletions
|
@ -155,7 +155,7 @@
|
|||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#49b9db5aae8db84ec21231444bf8345d450812b2",
|
||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||
"lbryinc": "lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
"lodash-es": "^4.17.14",
|
||||
|
|
|
@ -146,6 +146,7 @@ function ChannelContent(props: Props) {
|
|||
defaultFreshness={CS.FRESH_ALL}
|
||||
showHiddenByUser={viewHiddenChannels}
|
||||
forceShowReposts
|
||||
fetchViewCount
|
||||
hideFilters={!showFilters}
|
||||
hideAdvancedFilter={!showFilters}
|
||||
tileLayout={tileLayout}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doClaimSearch,
|
||||
selectClaimsByUri,
|
||||
selectClaimSearchByQuery,
|
||||
selectClaimSearchByQueryLastPageReached,
|
||||
selectFetchingClaimSearch,
|
||||
|
@ -12,11 +13,13 @@ import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
|||
import { makeSelectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||
import ClaimListDiscover from './view';
|
||||
import { doFetchViewCount } from 'lbryinc';
|
||||
|
||||
const select = (state) => ({
|
||||
followedTags: selectFollowedTags(state),
|
||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
loading: selectFetchingClaimSearch(state),
|
||||
showNsfw: selectShowMatureContent(state),
|
||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||
|
@ -29,6 +32,7 @@ const select = (state) => ({
|
|||
const perform = {
|
||||
doClaimSearch,
|
||||
doToggleTagFollowDesktop,
|
||||
doFetchViewCount,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ClaimListDiscover);
|
||||
|
|
|
@ -28,6 +28,7 @@ type Props = {
|
|||
meta?: Node,
|
||||
showNsfw: boolean,
|
||||
hideReposts: boolean,
|
||||
fetchViewCount?: boolean,
|
||||
history: { action: string, push: (string) => void, replace: (string) => void },
|
||||
location: { search: string, pathname: string },
|
||||
claimSearchByQuery: {
|
||||
|
@ -78,6 +79,8 @@ type Props = {
|
|||
showNoSourceClaims?: boolean,
|
||||
isChannel?: boolean,
|
||||
empty?: string,
|
||||
claimsByUri: { [string]: any },
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
};
|
||||
|
||||
function ClaimListDiscover(props: Props) {
|
||||
|
@ -94,6 +97,7 @@ function ClaimListDiscover(props: Props) {
|
|||
channelIds,
|
||||
showNsfw,
|
||||
hideReposts,
|
||||
fetchViewCount,
|
||||
history,
|
||||
location,
|
||||
mutedUris,
|
||||
|
@ -138,6 +142,8 @@ function ClaimListDiscover(props: Props) {
|
|||
isChannel = false,
|
||||
showNoSourceClaims,
|
||||
empty,
|
||||
claimsByUri,
|
||||
doFetchViewCount,
|
||||
} = props;
|
||||
const didNavigateForward = history.action === 'PUSH';
|
||||
const { search } = location;
|
||||
|
@ -518,6 +524,16 @@ function ClaimListDiscover(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
function fetchViewCountForUris(uris) {
|
||||
const claimIds = [];
|
||||
uris.forEach((uri) => {
|
||||
if (claimsByUri[uri]) {
|
||||
claimIds.push(claimsByUri[uri].claim_id);
|
||||
}
|
||||
});
|
||||
doFetchViewCount(claimIds.join(','));
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
|
@ -547,6 +563,12 @@ function ClaimListDiscover(props: Props) {
|
|||
}
|
||||
}, [uris, claimSearchResult, finalUris, setFinalUris, liveLivestreamsFirst, livestreamSearchResult]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (fetchViewCount) {
|
||||
fetchViewCountForUris(finalUris);
|
||||
}
|
||||
}, [finalUris]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const headerToUse = header || (
|
||||
<ClaimListHeader
|
||||
channelIds={channelIds}
|
||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
import UriIndicator from 'component/uriIndicator';
|
||||
import DateTime from 'component/dateTime';
|
||||
import Button from 'component/button';
|
||||
import FileViewCountInline from 'component/fileViewCountInline';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
|
@ -37,7 +38,14 @@ function ClaimPreviewSubtitle(props: Props) {
|
|||
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
||||
|
||||
{!isChannel &&
|
||||
(isLivestream && ENABLE_NO_SOURCE_CLAIMS ? __('Livestream') : <DateTime timeAgo uri={uri} />)}
|
||||
(isLivestream && ENABLE_NO_SOURCE_CLAIMS ? (
|
||||
__('Livestream')
|
||||
) : (
|
||||
<>
|
||||
<FileViewCountInline uri={uri} isLivestream={isLivestream} />
|
||||
<DateTime timeAgo uri={uri} />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
|
|
@ -7,6 +7,7 @@ import UriIndicator from 'component/uriIndicator';
|
|||
import TruncatedText from 'component/common/truncated-text';
|
||||
import DateTime from 'component/dateTime';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import FileViewCountInline from 'component/fileViewCountInline';
|
||||
import SubscribeButton from 'component/subscribeButton';
|
||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
|
@ -210,15 +211,11 @@ function ClaimPreviewTile(props: Props) {
|
|||
{!isChannel && (
|
||||
<React.Fragment>
|
||||
<div className="claim-preview__hover-actions">
|
||||
{isPlayable && (
|
||||
<FileWatchLaterLink focusable={false} uri={uri} />
|
||||
)}
|
||||
{isPlayable && <FileWatchLaterLink focusable={false} uri={uri} />}
|
||||
</div>
|
||||
{/* @if TARGET='app' */}
|
||||
<div className="claim-preview__hover-actions">
|
||||
{isStream && (
|
||||
<FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton />
|
||||
)}
|
||||
{isStream && <FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton />}
|
||||
</div>
|
||||
{/* @endif */}
|
||||
|
||||
|
@ -226,7 +223,6 @@ function ClaimPreviewTile(props: Props) {
|
|||
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
|
||||
)}
|
||||
{isCollection && (
|
||||
<React.Fragment>
|
||||
|
@ -264,7 +260,10 @@ function ClaimPreviewTile(props: Props) {
|
|||
|
||||
<div className="claim-tile__about">
|
||||
<UriIndicator uri={uri} link />
|
||||
<DateTime timeAgo uri={uri} />
|
||||
<div className="claim-tile__about--counts">
|
||||
<FileViewCountInline uri={uri} isLivestream={isLivestream} />
|
||||
<DateTime timeAgo uri={uri} />
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
SETTINGS,
|
||||
selectClaimsByUri,
|
||||
} from 'lbry-redux';
|
||||
import { doFetchViewCount } from 'lbryinc';
|
||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||
|
@ -25,6 +26,7 @@ const select = (state) => ({
|
|||
const perform = {
|
||||
doClaimSearch,
|
||||
doToggleTagFollowDesktop,
|
||||
doFetchViewCount,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ClaimListDiscover);
|
||||
|
|
|
@ -92,6 +92,7 @@ type Props = {
|
|||
livestreamMap?: { [string]: any },
|
||||
showNoSourceClaims?: boolean,
|
||||
renderProperties?: (Claim) => ?Node,
|
||||
fetchViewCount?: boolean,
|
||||
// claim search options are below
|
||||
tags: Array<string>,
|
||||
claimIds?: Array<string>,
|
||||
|
@ -117,6 +118,7 @@ type Props = {
|
|||
blockedUris: Array<string>,
|
||||
// --- perform ---
|
||||
doClaimSearch: ({}) => void,
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
};
|
||||
|
||||
function ClaimTilesDiscover(props: Props) {
|
||||
|
@ -126,6 +128,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
claimsByUri,
|
||||
showNsfw,
|
||||
hideReposts,
|
||||
fetchViewCount,
|
||||
// Below are options to pass that are forwarded to claim_search
|
||||
tags,
|
||||
channelIds,
|
||||
|
@ -150,6 +153,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
pinUrls,
|
||||
prefixUris,
|
||||
showNoSourceClaims,
|
||||
doFetchViewCount,
|
||||
} = props;
|
||||
|
||||
const { location } = useHistory();
|
||||
|
@ -294,6 +298,16 @@ function ClaimTilesDiscover(props: Props) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function fetchViewCountForUris(uris) {
|
||||
const claimIds = [];
|
||||
uris.forEach((uri) => {
|
||||
if (claimsByUri[uri]) {
|
||||
claimIds.push(claimsByUri[uri].claim_id);
|
||||
}
|
||||
});
|
||||
doFetchViewCount(claimIds.join(','));
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
|
@ -310,9 +324,14 @@ function ClaimTilesDiscover(props: Props) {
|
|||
|
||||
React.useEffect(() => {
|
||||
if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) {
|
||||
// Stash new results for next render cycle:
|
||||
setPrevUris(uris);
|
||||
// Fetch view count:
|
||||
if (fetchViewCount) {
|
||||
fetchViewCountForUris(uris);
|
||||
}
|
||||
}
|
||||
}, [shouldPerformSearch, prevUris, uris]);
|
||||
}, [shouldPerformSearch, prevUris, uris]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
|
13
ui/component/fileViewCountInline/index.js
Normal file
13
ui/component/fileViewCountInline/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { makeSelectViewCountForUri } from 'lbryinc';
|
||||
import FileViewCountInline from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
return {
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
viewCount: makeSelectViewCountForUri(props.uri)(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, null)(FileViewCountInline);
|
30
ui/component/fileViewCountInline/view.jsx
Normal file
30
ui/component/fileViewCountInline/view.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
isLivestream?: boolean,
|
||||
// --- select ---
|
||||
claim: ?StreamClaim,
|
||||
viewCount: string,
|
||||
};
|
||||
|
||||
export default function FileViewCountInline(props: Props) {
|
||||
const { isLivestream, claim, viewCount } = props;
|
||||
const formattedViewCount = Number(viewCount).toLocaleString();
|
||||
|
||||
if (!viewCount || (claim && claim.repost_url) || isLivestream) {
|
||||
// (1) Currently, makeSelectViewCountForUri doesn't differentiate between
|
||||
// unfetched view-count vs zero view-count. But since it's probably not
|
||||
// ideal to highlight that a view has 0 count, let's just not show anything.
|
||||
// (2) No idea how to get the repost src's claim ID from the repost claim,
|
||||
// so hiding it for now.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="view_count">
|
||||
{viewCount !== 1 ? __('%view_count% views', { view_count: formattedViewCount }) : __('1 view')}
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -598,6 +598,18 @@
|
|||
font-size: var(--font-body);
|
||||
}
|
||||
|
||||
.claim-tile__about--counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.view_count {
|
||||
&::after {
|
||||
content: '•';
|
||||
margin: 0 var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.claim-preview__file-property-overlay {
|
||||
position: absolute;
|
||||
bottom: var(--spacing-xxs);
|
||||
|
@ -681,14 +693,13 @@
|
|||
|
||||
// div that displays watch later button
|
||||
.claim-preview__hover-actions {
|
||||
|
||||
// if the user is using a mouse
|
||||
@media (pointer: fine){
|
||||
@media (pointer: fine) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// if the user doesn't have a pointer (mouse etc) hide watch later button
|
||||
@media (pointer: none), (pointer:coarse) {
|
||||
@media (pointer: none), (pointer: coarse) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -10148,9 +10148,9 @@ lbry-redux@lbryio/lbry-redux#49b9db5aae8db84ec21231444bf8345d450812b2:
|
|||
reselect "^3.0.0"
|
||||
uuid "^8.3.1"
|
||||
|
||||
lbryinc@lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59:
|
||||
lbryinc@lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/8f9a58bfc8312a65614fd7327661cdcc502c4e59"
|
||||
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/0b4e41ef90d6347819dd3453f2f9398a5c1b4f36"
|
||||
dependencies:
|
||||
reselect "^3.0.0"
|
||||
|
||||
|
|
Loading…
Reference in a new issue