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",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#49b9db5aae8db84ec21231444bf8345d450812b2",
|
"lbry-redux": "lbryio/lbry-redux#49b9db5aae8db84ec21231444bf8345d450812b2",
|
||||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
"lbryinc": "lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
"lodash-es": "^4.17.14",
|
"lodash-es": "^4.17.14",
|
||||||
|
|
|
@ -146,6 +146,7 @@ function ChannelContent(props: Props) {
|
||||||
defaultFreshness={CS.FRESH_ALL}
|
defaultFreshness={CS.FRESH_ALL}
|
||||||
showHiddenByUser={viewHiddenChannels}
|
showHiddenByUser={viewHiddenChannels}
|
||||||
forceShowReposts
|
forceShowReposts
|
||||||
|
fetchViewCount
|
||||||
hideFilters={!showFilters}
|
hideFilters={!showFilters}
|
||||||
hideAdvancedFilter={!showFilters}
|
hideAdvancedFilter={!showFilters}
|
||||||
tileLayout={tileLayout}
|
tileLayout={tileLayout}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
|
selectClaimsByUri,
|
||||||
selectClaimSearchByQuery,
|
selectClaimSearchByQuery,
|
||||||
selectClaimSearchByQueryLastPageReached,
|
selectClaimSearchByQueryLastPageReached,
|
||||||
selectFetchingClaimSearch,
|
selectFetchingClaimSearch,
|
||||||
|
@ -12,11 +13,13 @@ import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
import { makeSelectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import ClaimListDiscover from './view';
|
import ClaimListDiscover from './view';
|
||||||
|
import { doFetchViewCount } from 'lbryinc';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
claimSearchByQuery: selectClaimSearchByQuery(state),
|
claimSearchByQuery: selectClaimSearchByQuery(state),
|
||||||
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
|
claimSearchByQueryLastPageReached: selectClaimSearchByQueryLastPageReached(state),
|
||||||
|
claimsByUri: selectClaimsByUri(state),
|
||||||
loading: selectFetchingClaimSearch(state),
|
loading: selectFetchingClaimSearch(state),
|
||||||
showNsfw: selectShowMatureContent(state),
|
showNsfw: selectShowMatureContent(state),
|
||||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||||
|
@ -29,6 +32,7 @@ const select = (state) => ({
|
||||||
const perform = {
|
const perform = {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
doToggleTagFollowDesktop,
|
doToggleTagFollowDesktop,
|
||||||
|
doFetchViewCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(ClaimListDiscover);
|
export default connect(select, perform)(ClaimListDiscover);
|
||||||
|
|
|
@ -28,6 +28,7 @@ type Props = {
|
||||||
meta?: Node,
|
meta?: Node,
|
||||||
showNsfw: boolean,
|
showNsfw: boolean,
|
||||||
hideReposts: boolean,
|
hideReposts: boolean,
|
||||||
|
fetchViewCount?: boolean,
|
||||||
history: { action: string, push: (string) => void, replace: (string) => void },
|
history: { action: string, push: (string) => void, replace: (string) => void },
|
||||||
location: { search: string, pathname: string },
|
location: { search: string, pathname: string },
|
||||||
claimSearchByQuery: {
|
claimSearchByQuery: {
|
||||||
|
@ -78,6 +79,8 @@ type Props = {
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
isChannel?: boolean,
|
isChannel?: boolean,
|
||||||
empty?: string,
|
empty?: string,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
doFetchViewCount: (claimIdCsv: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimListDiscover(props: Props) {
|
function ClaimListDiscover(props: Props) {
|
||||||
|
@ -94,6 +97,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
channelIds,
|
channelIds,
|
||||||
showNsfw,
|
showNsfw,
|
||||||
hideReposts,
|
hideReposts,
|
||||||
|
fetchViewCount,
|
||||||
history,
|
history,
|
||||||
location,
|
location,
|
||||||
mutedUris,
|
mutedUris,
|
||||||
|
@ -138,6 +142,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
isChannel = false,
|
isChannel = false,
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
empty,
|
empty,
|
||||||
|
claimsByUri,
|
||||||
|
doFetchViewCount,
|
||||||
} = props;
|
} = props;
|
||||||
const didNavigateForward = history.action === 'PUSH';
|
const didNavigateForward = history.action === 'PUSH';
|
||||||
const { search } = location;
|
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]);
|
}, [uris, claimSearchResult, finalUris, setFinalUris, liveLivestreamsFirst, livestreamSearchResult]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (fetchViewCount) {
|
||||||
|
fetchViewCountForUris(finalUris);
|
||||||
|
}
|
||||||
|
}, [finalUris]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const headerToUse = header || (
|
const headerToUse = header || (
|
||||||
<ClaimListHeader
|
<ClaimListHeader
|
||||||
channelIds={channelIds}
|
channelIds={channelIds}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import FileViewCountInline from 'component/fileViewCountInline';
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -37,7 +38,14 @@ function ClaimPreviewSubtitle(props: Props) {
|
||||||
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
`${claimsInChannel} ${claimsInChannel === 1 ? __('upload') : __('uploads')}`}
|
||||||
|
|
||||||
{!isChannel &&
|
{!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>
|
</React.Fragment>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import UriIndicator from 'component/uriIndicator';
|
||||||
import TruncatedText from 'component/common/truncated-text';
|
import TruncatedText from 'component/common/truncated-text';
|
||||||
import DateTime from 'component/dateTime';
|
import DateTime from 'component/dateTime';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import FileViewCountInline from 'component/fileViewCountInline';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
@ -210,15 +211,11 @@ function ClaimPreviewTile(props: Props) {
|
||||||
{!isChannel && (
|
{!isChannel && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="claim-preview__hover-actions">
|
<div className="claim-preview__hover-actions">
|
||||||
{isPlayable && (
|
{isPlayable && <FileWatchLaterLink focusable={false} uri={uri} />}
|
||||||
<FileWatchLaterLink focusable={false} uri={uri} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
<div className="claim-preview__hover-actions">
|
<div className="claim-preview__hover-actions">
|
||||||
{isStream && (
|
{isStream && <FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton />}
|
||||||
<FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
|
@ -226,7 +223,6 @@ function ClaimPreviewTile(props: Props) {
|
||||||
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
|
<PreviewOverlayProperties uri={uri} properties={liveProperty || properties} />
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
{isCollection && (
|
{isCollection && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
@ -264,7 +260,10 @@ function ClaimPreviewTile(props: Props) {
|
||||||
|
|
||||||
<div className="claim-tile__about">
|
<div className="claim-tile__about">
|
||||||
<UriIndicator uri={uri} link />
|
<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>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
import { doFetchViewCount } from 'lbryinc';
|
||||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
|
@ -25,6 +26,7 @@ const select = (state) => ({
|
||||||
const perform = {
|
const perform = {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
doToggleTagFollowDesktop,
|
doToggleTagFollowDesktop,
|
||||||
|
doFetchViewCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(ClaimListDiscover);
|
export default connect(select, perform)(ClaimListDiscover);
|
||||||
|
|
|
@ -92,6 +92,7 @@ type Props = {
|
||||||
livestreamMap?: { [string]: any },
|
livestreamMap?: { [string]: any },
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
renderProperties?: (Claim) => ?Node,
|
renderProperties?: (Claim) => ?Node,
|
||||||
|
fetchViewCount?: boolean,
|
||||||
// claim search options are below
|
// claim search options are below
|
||||||
tags: Array<string>,
|
tags: Array<string>,
|
||||||
claimIds?: Array<string>,
|
claimIds?: Array<string>,
|
||||||
|
@ -117,6 +118,7 @@ type Props = {
|
||||||
blockedUris: Array<string>,
|
blockedUris: Array<string>,
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
doClaimSearch: ({}) => void,
|
doClaimSearch: ({}) => void,
|
||||||
|
doFetchViewCount: (claimIdCsv: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimTilesDiscover(props: Props) {
|
function ClaimTilesDiscover(props: Props) {
|
||||||
|
@ -126,6 +128,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
claimsByUri,
|
claimsByUri,
|
||||||
showNsfw,
|
showNsfw,
|
||||||
hideReposts,
|
hideReposts,
|
||||||
|
fetchViewCount,
|
||||||
// Below are options to pass that are forwarded to claim_search
|
// Below are options to pass that are forwarded to claim_search
|
||||||
tags,
|
tags,
|
||||||
channelIds,
|
channelIds,
|
||||||
|
@ -150,6 +153,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
pinUrls,
|
pinUrls,
|
||||||
prefixUris,
|
prefixUris,
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
|
doFetchViewCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { location } = useHistory();
|
const { location } = useHistory();
|
||||||
|
@ -294,6 +298,16 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
return undefined;
|
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(() => {
|
React.useEffect(() => {
|
||||||
if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) {
|
if (JSON.stringify(prevUris) !== JSON.stringify(uris) && !shouldPerformSearch) {
|
||||||
|
// Stash new results for next render cycle:
|
||||||
setPrevUris(uris);
|
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);
|
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 {
|
.claim-preview__file-property-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: var(--spacing-xxs);
|
bottom: var(--spacing-xxs);
|
||||||
|
@ -681,14 +693,13 @@
|
||||||
|
|
||||||
// div that displays watch later button
|
// div that displays watch later button
|
||||||
.claim-preview__hover-actions {
|
.claim-preview__hover-actions {
|
||||||
|
|
||||||
// if the user is using a mouse
|
// if the user is using a mouse
|
||||||
@media (pointer: fine){
|
@media (pointer: fine) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the user doesn't have a pointer (mouse etc) hide watch later button
|
// 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;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10148,9 +10148,9 @@ lbry-redux@lbryio/lbry-redux#49b9db5aae8db84ec21231444bf8345d450812b2:
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
uuid "^8.3.1"
|
uuid "^8.3.1"
|
||||||
|
|
||||||
lbryinc@lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59:
|
lbryinc@lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36:
|
||||||
version "0.0.1"
|
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:
|
dependencies:
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue