// @flow import * as CS from 'constants/claim_search'; import * as ICONS from 'constants/icons'; import * as SETTINGS from 'constants/settings'; import type { Node } from 'react'; import classnames from 'classnames'; import React from 'react'; import usePersistedState from 'effects/use-persisted-state'; import { useHistory } from 'react-router'; import { FormField } from 'component/common/form'; import Button from 'component/button'; import { toCapitalCase } from 'util/string'; import SEARCHABLE_LANGUAGES from 'constants/searchable_languages'; type Props = { defaultTags: string, freshness?: string, defaultFreshness?: string, claimType?: Array<string>, streamType?: string | Array<string>, defaultStreamType?: string | Array<string>, feeAmount: string, sortBy?: string, orderBy?: Array<string>, defaultOrderBy?: string, hideAdvancedFilter: boolean, hasMatureTags: boolean, hiddenNsfwMessage?: Node, channelIds?: Array<string>, tileLayout: boolean, doSetClientSetting: (string, boolean, ?boolean) => void, setPage: (number) => void, hideFilters: boolean, searchInLanguage: boolean, languageSetting: string, scrollAnchor?: string, }; function ClaimListHeader(props: Props) { const { defaultTags, freshness, defaultFreshness, claimType, streamType, defaultStreamType, feeAmount, sortBy, orderBy, defaultOrderBy, hideAdvancedFilter, hasMatureTags, hiddenNsfwMessage, channelIds, tileLayout, doSetClientSetting, setPage, hideFilters, searchInLanguage, languageSetting, scrollAnchor, } = props; const { action, push, location } = useHistory(); const { search } = location; const [expanded, setExpanded] = usePersistedState(`expanded-${location.pathname}`, false); const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING); const urlParams = new URLSearchParams(search); const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness; const contentTypeParam = urlParams.get(CS.CONTENT_KEY); const streamTypeParam = streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null; const durationParam = urlParams.get(CS.DURATION_KEY) || null; const languageParam = urlParams.get(CS.LANGUAGE_KEY) || null; const sortByParam = sortBy || urlParams.get(CS.SORT_BY_KEY) || null; const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY); const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds; const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY; const showDuration = !claimType || (claimType && claimType !== CS.CLAIM_CHANNEL && claimType !== CS.CLAIM_COLLECTION); const isFiltered = () => Boolean( urlParams.get(CS.FRESH_KEY) || urlParams.get(CS.CONTENT_KEY) || urlParams.get(CS.DURATION_KEY) || urlParams.get(CS.TAGS_KEY) || urlParams.get(CS.FEE_AMOUNT_KEY) || urlParams.get(CS.LANGUAGE_KEY) ); const languageValue = searchInLanguage ? languageParam === null ? languageSetting : languageParam : languageParam === null ? CS.LANGUAGES_ALL : languageParam; const shouldHighlight = searchInLanguage ? languageParam !== languageSetting && languageParam !== null : languageParam !== CS.LANGUAGES_ALL && languageParam !== null; React.useEffect(() => { if (action !== 'POP' && isFiltered()) { setExpanded(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy; if (!orderParam) { if (action === 'POP') { // Reaching here means user have popped back to the page's entry point (e.g. '/$/tags' without any '?order='). orderParam = orderParamEntry; } else { // This is the direct entry into the page, so we load the user's previous value. orderParam = orderParamUser; } } React.useEffect(() => { setOrderParamUser(orderParam); }, [orderParam, setOrderParamUser]); React.useEffect(() => { // One-time update to stash the finalized 'orderParam' at entry. if (action !== 'POP') { setOrderParamEntry(orderParam); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function handleChange(change) { const url = buildUrl(change); setPage(1); push(url); } function handleAdvancedReset() { const newUrlParams = new URLSearchParams(search); newUrlParams.delete('claim_type'); newUrlParams.delete('channel_ids'); const newSearch = `?${newUrlParams.toString()}`; push(newSearch); } function buildUrl(delta) { const newUrlParams = new URLSearchParams(location.search); CS.KEYS.forEach((k) => { // $FlowFixMe get() can return null if (urlParams.get(k) !== null) newUrlParams.set(k, urlParams.get(k)); }); switch (delta.key) { case CS.ORDER_BY_KEY: newUrlParams.set(CS.ORDER_BY_KEY, delta.value); break; case CS.SORT_BY_KEY: if (delta.value === CS.SORT_BY.NEWEST.key) { newUrlParams.delete(CS.SORT_BY_KEY); } else { newUrlParams.set(CS.SORT_BY_KEY, delta.value); } break; case CS.FRESH_KEY: if (delta.value === defaultFreshness || delta.value === CS.FRESH_DEFAULT) { newUrlParams.delete(CS.FRESH_KEY); } else { newUrlParams.set(CS.FRESH_KEY, delta.value); } break; case CS.CONTENT_KEY: if ( delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST || delta.value === CS.CLAIM_COLLECTION ) { newUrlParams.delete(CS.DURATION_KEY); newUrlParams.set(CS.CONTENT_KEY, delta.value); } else if (delta.value === CS.CONTENT_ALL) { newUrlParams.delete(CS.CONTENT_KEY); } else { newUrlParams.set(CS.CONTENT_KEY, delta.value); } break; case CS.DURATION_KEY: if (delta.value === CS.DURATION_ALL) { newUrlParams.delete(CS.DURATION_KEY); } else { newUrlParams.set(CS.DURATION_KEY, delta.value); } break; case CS.LANGUAGE_KEY: newUrlParams.set(CS.LANGUAGE_KEY, delta.value); break; case CS.TAGS_KEY: if (delta.value === CS.TAGS_ALL) { if (defaultTags === CS.TAGS_ALL) { newUrlParams.delete(CS.TAGS_KEY); } else { newUrlParams.set(CS.TAGS_KEY, delta.value); } } else if (delta.value === CS.TAGS_FOLLOWED) { if (defaultTags === CS.TAGS_FOLLOWED) { newUrlParams.delete(CS.TAGS_KEY); } else { newUrlParams.set(CS.TAGS_KEY, delta.value); // redundant but special } } else { newUrlParams.set(CS.TAGS_KEY, delta.value); } break; case CS.FEE_AMOUNT_KEY: if (delta.value === CS.FEE_AMOUNT_ANY) { newUrlParams.delete(CS.FEE_AMOUNT_KEY); } else { newUrlParams.set(CS.FEE_AMOUNT_KEY, delta.value); } break; } return `?${newUrlParams.toString()}` + (scrollAnchor ? '#' + scrollAnchor : ''); } return ( <> <div className="claim-search__wrapper"> <div className="claim-search__top"> {!hideFilters && ( <div className="claim-search__menu-group"> {CS.ORDER_BY_TYPES.map((type) => ( <Button key={type} onClick={(e) => handleChange({ key: CS.ORDER_BY_KEY, value: type, }) } className={classnames(`button-toggle button-toggle--${type}`, { 'button-toggle--active': orderParam === type, })} disabled={orderBy} icon={toCapitalCase(type)} iconSize={toCapitalCase(type) === ICONS.NEW ? 20 : undefined} label={__(toCapitalCase(type))} /> ))} </div> )} <div className="claim-search__menu-group"> {!hideAdvancedFilter && ( <Button aria-label={__('More')} className={classnames(`button-toggle button-toggle--top button-toggle--more`, { 'button-toggle--custom': isFiltered(), 'button-toggle--active': expanded, })} icon={ICONS.SLIDERS} onClick={() => setExpanded(!expanded)} /> )} {tileLayout !== undefined && ( <Button onClick={() => { doSetClientSetting(SETTINGS.TILE_LAYOUT, !tileLayout); }} button="alt" className="button-toggle" aria-label={tileLayout ? __('Change to list layout') : __('Change to tile layout')} icon={ICONS.LAYOUT} /> )} </div> </div> {expanded && ( <> <div className={classnames(`claim-search__menus`)}> {/* FRESHNESS FIELD */} {orderParam === CS.ORDER_BY_TOP && ( <div className="claim-search__input-container"> <FormField className={classnames('claim-search__dropdown', { 'claim-search__dropdown--selected': freshnessParam !== defaultFreshness, })} type="select" name="trending_time" label={__('How Fresh')} value={freshnessParam} onChange={(e) => handleChange({ key: CS.FRESH_KEY, value: e.target.value, }) } > {CS.FRESH_TYPES.map((time) => ( <option key={time} value={time}> {/* i18fixme */} {time === CS.FRESH_DAY && __('Today')} { time !== CS.FRESH_ALL && time !== CS.FRESH_DEFAULT && time !== CS.FRESH_DAY && __('This ' + toCapitalCase(time)) /* yes, concat before i18n, since it is read from const */ } {time === CS.FRESH_ALL && __('All time')} {time === CS.FRESH_DEFAULT && __('Default')} </option> ))} </FormField> </div> )} {/* CONTENT_TYPES FIELD */} {!claimType && ( <div className={classnames('claim-search__input-container', { 'claim-search__input-container--selected': contentTypeParam, })} > <FormField className={classnames('claim-search__dropdown', { 'claim-search__dropdown--selected': contentTypeParam, })} type="select" name="claimType" label={__('Content Type')} value={contentTypeParam || CS.CONTENT_ALL} onChange={(e) => handleChange({ key: CS.CONTENT_KEY, value: e.target.value, }) } > {CS.CONTENT_TYPES.map((type) => { if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) { return ( <option key={type} value={type}> {/* i18fixme */} {type === CS.CLAIM_COLLECTION && __('List')} {type === CS.CLAIM_CHANNEL && __('Channel')} {type === CS.CLAIM_REPOST && __('Repost')} {type === CS.FILE_VIDEO && __('Video')} {type === CS.FILE_AUDIO && __('Audio')} {type === CS.FILE_IMAGE && __('Image')} {type === CS.FILE_MODEL && __('Model')} {type === CS.FILE_BINARY && __('Other')} {type === CS.FILE_DOCUMENT && __('Document')} {type === CS.CONTENT_ALL && __('Any')} </option> ); } })} </FormField> </div> )} {/* LANGUAGE FIELD */} { <div className={classnames('claim-search__input-container', { 'claim-search__input-container--selected': shouldHighlight, })} > <FormField className={classnames('claim-search__dropdown', { 'claim-search__dropdown--selected': shouldHighlight, })} type="select" name="claimType" label={__('Language')} value={languageValue || CS.LANGUAGES_ALL} onChange={(e) => handleChange({ key: CS.LANGUAGE_KEY, value: e.target.value, }) } > <option key={CS.LANGUAGES_ALL} value={CS.LANGUAGES_ALL}> {__('Any')} {/* i18fixme */} </option> {Object.entries(SEARCHABLE_LANGUAGES).map(([code, label]) => { return ( <option key={code} value={code}> {String(label)} </option> ); })} </FormField> </div> } {/* DURATIONS FIELD */} {showDuration && ( <div className={'claim-search__input-container'}> <FormField className={classnames('claim-search__dropdown', { 'claim-search__dropdown--selected': durationParam, })} label={__('Duration')} type="select" name="duration" disabled={ !( contentTypeParam === null || streamTypeParam === CS.FILE_AUDIO || streamTypeParam === CS.FILE_VIDEO ) } value={durationParam || CS.DURATION_ALL} onChange={(e) => handleChange({ key: CS.DURATION_KEY, value: e.target.value, }) } > {CS.DURATION_TYPES.map((dur) => ( <option key={dur} value={dur}> {/* i18fixme */} {dur === CS.DURATION_SHORT && __('Short (< 4 min)')} {dur === CS.DURATION_MEDIUM && __('Medium (4 - 20 min)')} {dur === CS.DURATION_LONG && __('Long (> 20 min)')} {dur === CS.DURATION_ALL && __('Any')} </option> ))} </FormField> </div> )} {/* PAID FIELD */} {claimType !== CS.CLAIM_CHANNEL && ( <div className={'claim-search__input-container'}> <FormField className={classnames('claim-search__dropdown', { 'claim-search__dropdown--selected': feeAmountParam === CS.FEE_AMOUNT_ONLY_FREE || feeAmountParam === CS.FEE_AMOUNT_ONLY_PAID, })} label={__('Price')} type="select" name="paidcontent" value={feeAmountParam} onChange={(e) => handleChange({ key: CS.FEE_AMOUNT_KEY, value: e.target.value, }) } > <option value={CS.FEE_AMOUNT_ANY}>{__('Any')}</option> <option value={CS.FEE_AMOUNT_ONLY_FREE}>{__('Free')}</option> <option value={CS.FEE_AMOUNT_ONLY_PAID}>{__('Paid')}</option> ))} </FormField> </div> )} {/* SORT FIELD */} {orderParam === CS.ORDER_BY_NEW && ( <div className={'claim-search__input-container'}> <FormField className={classnames('claim-search__dropdown', { 'claim-search__dropdown--selected': sortByParam, })} label={__('Sort By')} type="select" name="sort_by" value={sortByParam || CS.SORT_BY.NEWEST.key} onChange={(e) => handleChange({ key: CS.SORT_BY_KEY, value: e.target.value })} > {Object.entries(CS.SORT_BY).map(([key, value]) => { return ( // $FlowFixMe https://github.com/facebook/flow/issues/2221 <option key={value.key} value={value.key}> {/* $FlowFixMe */} {__(value.str)} </option> ); })} </FormField> </div> )} {channelIdsInUrl && ( <div className={'claim-search__input-container'}> <label>{__('Advanced Filters from URL')}</label> <Button button="alt" className="claim-search__filter-button" label={__('Clear')} onClick={handleAdvancedReset} /> </div> )} </div> </> )} </div> {hasMatureTags && hiddenNsfwMessage} </> ); } export default ClaimListHeader;