Allow users to hide content they've already viewed

This commit is contained in:
David Garrett 2022-11-21 19:12:54 -05:00
parent f36282555c
commit 17332979b6
9 changed files with 156 additions and 54 deletions

Binary file not shown.

View file

@ -106,6 +106,7 @@ function ChannelForm(props: Props) {
const languageParam = params.languages; const languageParam = params.languages;
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0]; const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1]; const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
const [hideWatched, setHideWatched] = usePersistedState('hideWatched', false); // UPDATE: Experimenting with hiding watched content
const submitLabel = React.useMemo(() => { const submitLabel = React.useMemo(() => {
if (isClaimingInitialRewards) { if (isClaimingInitialRewards) {
return __('Claiming credits...'); return __('Claiming credits...');
@ -240,7 +241,24 @@ function ChannelForm(props: Props) {
errorMsg = __('Invalid %error_type%', { error_type: (thumbError && 'thumbnail') || (coverError && 'cover image') }); errorMsg = __('Invalid %error_type%', { error_type: (thumbError && 'thumbnail') || (coverError && 'cover image') });
} }
React.useEffect(() => { // UPDATE: Add "Hide Watched" to channel pages
function getHideWatchedElem() {
return (
<div className={classnames(`card claim-search__menus`)}>
<FormField
label={__('Hide Watched')}
name="hide_watched"
type="checkbox"
checked={hideWatched}
onChange={() => {
setHideWatched((prev) => !prev);
}}
/>
</div>
);
}
React.useEffect(() => {
let nameError; let nameError;
if (!name && name !== undefined) { if (!name && name !== undefined) {
nameError = __('A name is required for your url'); nameError = __('A name is required for your url');
@ -525,7 +543,8 @@ function ChannelForm(props: Props) {
<div className="section__actions"> <div className="section__actions">
<ClaimAbandonButton uri={uri} abandonActionCallback={() => replace(`/$/${PAGES.CHANNELS}`)} /> <ClaimAbandonButton uri={uri} abandonActionCallback={() => replace(`/$/${PAGES.CHANNELS}`)} />
</div> </div>
)} )}
{getHideWatchedElem()}
</> </>
} }
/> />

View file

@ -318,25 +318,21 @@ function ClaimListDiscover(props: Props) {
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) { if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`; options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) { } else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
// Warning - hack below // UPDATE: Commented out these lines of code to truly sort by oldest first...
// If users are following more than 10 channels or tags, limit results to stuff less than a year old //if (
// For more than 20, drop it down to 6 months // (options.channel_ids && options.channel_ids.length > 20) ||
// This helps with timeout issues for users that are following a ton of stuff // (options.any_tags && options.any_tags.length > 20)
// https://github.com/lbryio/lbry-sdk/issues/2420 //) {
if ( // options.release_time = `>${Math.floor(moment().subtract(3, CS.FRESH_MONTH).startOf('week').unix())}`;
(options.channel_ids && options.channel_ids.length > 20) || //} else if (
(options.any_tags && options.any_tags.length > 20) // (options.channel_ids && options.channel_ids.length > 10) ||
) { // (options.any_tags && options.any_tags.length > 10)
options.release_time = `>${Math.floor(moment().subtract(3, CS.FRESH_MONTH).startOf('week').unix())}`; //) {
} else if ( // options.release_time = `>${Math.floor(moment().subtract(1, CS.FRESH_YEAR).startOf('week').unix())}`;
(options.channel_ids && options.channel_ids.length > 10) || //} else {
(options.any_tags && options.any_tags.length > 10) // // Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
) { // options.release_time = `<${Math.floor(moment().startOf('minute').unix())}`;
options.release_time = `>${Math.floor(moment().subtract(1, CS.FRESH_YEAR).startOf('week').unix())}`; //}
} else {
// Hack for at least the New page until https://github.com/lbryio/lbry-sdk/issues/2591 is fixed
options.release_time = `<${Math.floor(moment().startOf('minute').unix())}`;
}
} }
} }

View file

@ -67,6 +67,7 @@ function ClaimListHeader(props: Props) {
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness; const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
const [hideWatched, setHideWatched] = usePersistedState('hideWatched', false); // UPDATE: Experimenting with hiding watched content
const contentTypeParam = urlParams.get(CS.CONTENT_KEY); const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
const streamTypeParam = const streamTypeParam =
streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null; streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null;
@ -97,7 +98,24 @@ function ClaimListHeader(props: Props) {
const shouldHighlight = searchInLanguage const shouldHighlight = searchInLanguage
? languageParam !== languageSetting && languageParam !== null ? languageParam !== languageSetting && languageParam !== null
: languageParam !== CS.LANGUAGES_ALL && languageParam !== null; : languageParam !== CS.LANGUAGES_ALL && languageParam !== null;
// UPDATE: Experimenting with hiding watched content
// Adding a Hide Watched checkbox to the main menu
function getHideWatchedElem() {
return (
<div className={`claim-search__checkbox`}>
<FormField
name="hide_watched"
type="checkbox"
checked={hideWatched}
onChange={() => {
setHideWatched((prev) => !prev);
}}
/>
</div>
);
}
React.useEffect(() => { React.useEffect(() => {
if (action !== 'POP' && isFiltered()) { if (action !== 'POP' && isFiltered()) {
@ -481,10 +499,10 @@ function ClaimListHeader(props: Props) {
</option> </option>
); );
})} })}
</FormField> </FormField>
</div> </div>
)}
)}
{channelIdsInUrl && ( {channelIdsInUrl && (
<div className={'claim-search__input-container'}> <div className={'claim-search__input-container'}>
<label>{__('Advanced Filters from URL')}</label> <label>{__('Advanced Filters from URL')}</label>
@ -495,10 +513,14 @@ function ClaimListHeader(props: Props) {
onClick={handleAdvancedReset} onClick={handleAdvancedReset}
/> />
</div> </div>
)} )}
<div className={'checkbox-label'}>
<label>Hide Watched Content</label>
{getHideWatchedElem()}
</div>
</div> </div>
</> </>
)} )}
</div> </div>
{hasMatureTags && hiddenNsfwMessage} {hasMatureTags && hiddenNsfwMessage}

View file

@ -21,6 +21,7 @@ import { selectIsSubscribedForUri } from 'redux/selectors/subscriptions';
import { isClaimNsfw } from 'util/claim'; import { isClaimNsfw } from 'util/claim';
import ClaimPreview from './view'; import ClaimPreview from './view';
import formatMediaDuration from 'util/formatMediaDuration'; import formatMediaDuration from 'util/formatMediaDuration';
import { makeSelectContentWatchedPercentageForUri } from 'redux/selectors/content'; // UPDATE: Added mSCWPFU (Watched content)
const select = (state, props) => { const select = (state, props) => {
const claim = props.uri && selectClaimForUri(state, props.uri); const claim = props.uri && selectClaimForUri(state, props.uri);
@ -46,6 +47,7 @@ const select = (state, props) => {
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state), wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state), isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
lang: selectLanguage(state), lang: selectLanguage(state),
isWatched: makeSelectContentWatchedPercentageForUri(props.uri)(state) > 80, // UPDATE: Added isWatched, getting percent watched
}; };
}; };

View file

@ -32,6 +32,7 @@ import ClaimPreviewNoContent from './claim-preview-no-content';
import CollectionEditButtons from 'component/collectionEditButtons'; import CollectionEditButtons from 'component/collectionEditButtons';
import { useIsMobile } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import AbandonedChannelPreview from 'component/abandonedChannelPreview'; import AbandonedChannelPreview from 'component/abandonedChannelPreview';
import usePersistedState from 'effects/use-persisted-state'; // UPDATE: usePersistedState is required for watched content
// preview images used on the landing page and on the channel page // preview images used on the landing page and on the channel page
type Props = { type Props = {
@ -82,6 +83,7 @@ type Props = {
showEdit?: boolean, showEdit?: boolean,
dragHandleProps?: any, dragHandleProps?: any,
unavailableUris?: Array<string>, unavailableUris?: Array<string>,
isWatched: boolean, // UPDATE: Declare isWatched variable
}; };
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -141,10 +143,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
showEdit, showEdit,
dragHandleProps, dragHandleProps,
unavailableUris, unavailableUris,
isWatched, // UPDATE: Variables to use in the ClaimPreviewTile
} = props; } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [hideWatched, setHideWatched] = usePersistedState('hideWatched', false); //UPDATE: Use hideWatched
const isCollection = claim && claim.value_type === 'collection'; const isCollection = claim && claim.value_type === 'collection';
const collectionClaimId = isCollection && claim && claim.claim_id; const collectionClaimId = isCollection && claim && claim.claim_id;
const listId = collectionId || collectionClaimId; const listId = collectionId || collectionClaimId;
@ -253,8 +256,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
if (customShouldHide(claim)) { if (customShouldHide(claim)) {
shouldHide = true; shouldHide = true;
} }
} }
// Weird placement warning // Weird placement warning
// Make sure this happens after we figure out if this claim needs to be hidden // Make sure this happens after we figure out if this claim needs to be hidden
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, shouldHide); const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, shouldHide);
@ -272,15 +276,25 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
} }
} }
useEffect(() => { useEffect(() => {
if (isValid && !isResolvingUri && shouldFetch && uri) { if (isValid && !isResolvingUri && shouldFetch && uri) {
resolveUri(uri); resolveUri(uri);
} }
}, [isValid, uri, isResolvingUri, shouldFetch, resolveUri]); }, [isValid, uri, isResolvingUri, shouldFetch, resolveUri]);
if (shouldHide && !showNullPlaceholder) { // UPDATE: Hiding watched content
if (isWatched && hideWatched) {
shouldHide = true;
}
if (shouldHide) {
return null;
}
// END OF UPDATE:
if (shouldHide && !showNullPlaceholder) {
return null; return null;
} }
if (placeholder === 'loading' || (uri && !claim && isResolvingUri)) { if (placeholder === 'loading' || (uri && !claim && isResolvingUri)) {
return <ClaimPreviewLoading isChannel={isChannelUri} type={type} />; return <ClaimPreviewLoading isChannel={isChannelUri} type={type} />;

View file

@ -10,6 +10,7 @@ import { doFileGet } from 'redux/actions/file';
import { doResolveUri } from 'redux/actions/claims'; import { doResolveUri } from 'redux/actions/claims';
import { selectViewCountForUri, selectBanStateForUri } from 'lbryinc'; import { selectViewCountForUri, selectBanStateForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectContentWatchedPercentageForUri } from 'redux/selectors/content'; // UPDATE: Added percentage watched element
import { isClaimNsfw } from 'util/claim'; import { isClaimNsfw } from 'util/claim';
import ClaimPreviewTile from './view'; import ClaimPreviewTile from './view';
import formatMediaDuration from 'util/formatMediaDuration'; import formatMediaDuration from 'util/formatMediaDuration';
@ -30,6 +31,7 @@ const select = (state, props) => {
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),
isMature: claim ? isClaimNsfw(claim) : false, isMature: claim ? isClaimNsfw(claim) : false,
viewCount: selectViewCountForUri(state, props.uri), viewCount: selectViewCountForUri(state, props.uri),
isWatched: makeSelectContentWatchedPercentageForUri(props.uri)(state) > 80, // UPDATE: Get isWatched view percentage and only show < 80%
}; };
}; };

View file

@ -20,6 +20,7 @@ import ClaimMenuList from 'component/claimMenuList';
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay'; import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
// $FlowFixMe cannot resolve ... // $FlowFixMe cannot resolve ...
import PlaceholderTx from 'static/img/placeholderTx.gif'; import PlaceholderTx from 'static/img/placeholderTx.gif';
import usePersistedState from 'effects/use-persisted-state'; // UPDATE: usePersistedState is required for watched content
type Props = { type Props = {
uri: string, uri: string,
@ -42,6 +43,7 @@ type Props = {
collectionId?: string, collectionId?: string,
viewCount: string, viewCount: string,
swipeLayout: boolean, swipeLayout: boolean,
isWatched: boolean, // UPDATE: Declare isWatched variable
}; };
// preview image cards used in related video functionality, channel overview page and homepage // preview image cards used in related video functionality, channel overview page and homepage
@ -67,10 +69,12 @@ function ClaimPreviewTile(props: Props) {
mediaDuration, mediaDuration,
viewCount, viewCount,
swipeLayout = false, swipeLayout = false,
isWatched, // UPDATE: Variables to use in the ClaimPreviewTile
} = props; } = props;
const isRepost = claim && claim.repost_channel_url; const isRepost = claim && claim.repost_channel_url;
const isCollection = claim && claim.value_type === 'collection'; const isCollection = claim && claim.value_type === 'collection';
const isStream = claim && claim.value_type === 'stream'; const isStream = claim && claim.value_type === 'stream';
const [hideWatched, setHideWatched] = usePersistedState('hideWatched', false); //UPDATE: Use hideWatched
// $FlowFixMe // $FlowFixMe
const isPlayable = const isPlayable =
claim && claim &&
@ -126,18 +130,23 @@ function ClaimPreviewTile(props: Props) {
let shouldHide = false; let shouldHide = false;
if (isMature && !showMature) { // UPDATE: Hiding watched content
// Unfortunately needed until this is resolved if (isMature && !showMature) {
// https://github.com/lbryio/lbry-sdk/issues/2785 // Unfortunately needed until this is resolved
shouldHide = true; // https://github.com/lbryio/lbry-sdk/issues/2785
} else { shouldHide = true;
shouldHide = } else {
banState.blacklisted || banState.filtered || (!showHiddenByUser && (banState.muted || banState.blocked)); shouldHide =
} banState.blacklisted ||
banState.filtered ||
(!showHiddenByUser && (banState.muted || banState.blocked)) ||
(isWatched && hideWatched);
}
if (shouldHide) { if (shouldHide) {
return null; return null;
} }
// END OF UPDATE:
const isChannelPage = location.pathname.startsWith('/@'); const isChannelPage = location.pathname.startsWith('/@');

View file

@ -6,6 +6,7 @@ import { Form, FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import classnames from 'classnames'; import classnames from 'classnames';
import usePersistedState from 'effects/use-persisted-state';
const CLAIM_TYPES = { const CLAIM_TYPES = {
[SEARCH_OPTIONS.INCLUDE_FILES]: 'Files', [SEARCH_OPTIONS.INCLUDE_FILES]: 'Files',
@ -59,9 +60,11 @@ const SearchOptions = (props: Props) => {
if (simple) { if (simple) {
delete TYPES_ADVANCED[SEARCH_OPTIONS.MEDIA_APPLICATION]; delete TYPES_ADVANCED[SEARCH_OPTIONS.MEDIA_APPLICATION];
delete TYPES_ADVANCED[SEARCH_OPTIONS.MEDIA_IMAGE]; delete TYPES_ADVANCED[SEARCH_OPTIONS.MEDIA_IMAGE];
} }
React.useEffect(() => { const [hideWatched, setHideWatched] = usePersistedState('hideWatched', false); // UPDATE: Experimenting with hiding watched content
React.useEffect(() => {
// We no longer let the user set the search results count, but the value // We no longer let the user set the search results count, but the value
// will be in local storage for existing users. Override that. // will be in local storage for existing users. Override that.
if (options[SEARCH_OPTIONS.RESULT_COUNT] !== SEARCH_PAGE_SIZE) { if (options[SEARCH_OPTIONS.RESULT_COUNT] !== SEARCH_PAGE_SIZE) {
@ -69,7 +72,32 @@ const SearchOptions = (props: Props) => {
} }
}, []); }, []);
function updateSearchOptions(option, value) { // UPDATE: Adding Hide Watched Content checkbox to search
function getHideWatchedElem() {
return (
<div className={`claim-search__checkbox_searchbox`}>
<FormField
name="hide_watched"
type="checkbox"
checked={hideWatched}
onChange={() => {
setHideWatched((prev) => !prev);
}}
/>
<Icon
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
customTooltipText={__(
'Hide content you have already viewed from search results.'
)}
/>
</div>
);
}
function updateSearchOptions(option, value) {
setSearchOption(option, value); setSearchOption(option, value);
if (onSearchOptionsChanged) { if (onSearchOptionsChanged) {
onSearchOptionsChanged(option); onSearchOptionsChanged(option);
@ -149,7 +177,8 @@ const SearchOptions = (props: Props) => {
</> </>
); );
const otherOptionsElem = ( // UPDATE: Changed element name to exactMatchElem
const exactMatchElem = (
<> <>
<div className="filter-values"> <div className="filter-values">
<FormField <FormField
@ -157,7 +186,6 @@ const SearchOptions = (props: Props) => {
name="exact-match" name="exact-match"
checked={options[SEARCH_OPTIONS.EXACT]} checked={options[SEARCH_OPTIONS.EXACT]}
onChange={() => updateSearchOptions(SEARCH_OPTIONS.EXACT, !options[SEARCH_OPTIONS.EXACT])} onChange={() => updateSearchOptions(SEARCH_OPTIONS.EXACT, !options[SEARCH_OPTIONS.EXACT])}
label={__('Exact match')}
/> />
<Icon <Icon
className="icon--help" className="icon--help"
@ -167,8 +195,9 @@ const SearchOptions = (props: Props) => {
customTooltipText={__( customTooltipText={__(
'Find results that include all the given words in the exact order.\nThis can also be done by surrounding the search query with quotation marks (e.g. "hello world").' 'Find results that include all the given words in the exact order.\nThis can also be done by surrounding the search query with quotation marks (e.g. "hello world").'
)} )}
/> />
</div> </div>
</> </>
); );
@ -192,7 +221,14 @@ const SearchOptions = (props: Props) => {
onClick={() => updateSearchOptions(SEARCH_OPTIONS.TIME_FILTER, '')} onClick={() => updateSearchOptions(SEARCH_OPTIONS.TIME_FILTER, '')}
/> />
</div> </div>
); );
// UPDATE: Declare constant for the Hide Watch Content element
const hideWatchedElem = (
<div>
{getHideWatchedElem()}
</div>
);
const sortByElem = ( const sortByElem = (
<div className="filter-values"> <div className="filter-values">
@ -208,10 +244,11 @@ const SearchOptions = (props: Props) => {
</div> </div>
); );
const uploadDateLabel = const uploadDateLabel =
options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS ? __('Creation Date') : __('Upload Date'); options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS ? __('Creation Date') : __('Upload Date');
return ( // UPDATE: Added row to table for hiding watched content in search settings
return (
<div> <div>
<Button <Button
button="alt" button="alt"
@ -230,7 +267,8 @@ const SearchOptions = (props: Props) => {
{addRow(__('Type'), typeElem)} {addRow(__('Type'), typeElem)}
{addRow(uploadDateLabel, uploadDateElem)} {addRow(uploadDateLabel, uploadDateElem)}
{addRow(__('Sort By'), sortByElem)} {addRow(__('Sort By'), sortByElem)}
{addRow(__('Other Options'), otherOptionsElem)} {addRow(__('Exact Match'), exactMatchElem)}
{addRow(__('Hide Watched Content'), hideWatchedElem)}
</tbody> </tbody>
</table> </table>
</Form> </Form>