Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function AdditionalFilters(props: Props) {
name="hide_reposts"
type="checkbox"
checked={filterCtx.repost.hideReposts}
disabled={contentType === CS.CLAIM_REPOST}
disabled={filterCtx.isChannelSearch || contentType === CS.CLAIM_REPOST}
onChange={() => filterCtx.repost.setHideReposts((prev) => !prev)}
/>
</div>
Expand All @@ -40,6 +40,7 @@ function AdditionalFilters(props: Props) {
name="hide_members_only"
type="checkbox"
checked={filterCtx.membersOnly.hideMembersOnly}
disabled={filterCtx.isChannelSearch}
onChange={() => filterCtx.membersOnly.setHideMembersOnly((prev) => !prev)}
/>
</div>
Expand Down
9 changes: 6 additions & 3 deletions ui/component/claimListHeader/view.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ function ClaimListHeader(props: Props) {
label={__('Price')}
type="select"
name="paidcontent"
disabled={filterCtx.isChannelSearch}
value={feeAmountParam}
onChange={(e) =>
handleChange({
Expand Down Expand Up @@ -594,9 +595,11 @@ function ClaimListHeader(props: Props) {
<AdditionalFilters filterCtx={filterCtx} contentType={contentTypeParam} />
</div>
)}
<div className="claim-search__input-container">
{!filterCtx?.liftUpTagSearch && <TagSearch urlParams={urlParams} handleChange={handleChange} />}
</div>
{!filterCtx.isChannelSearch && (
<div className="claim-search__input-container">
{!filterCtx?.liftUpTagSearch && <TagSearch urlParams={urlParams} handleChange={handleChange} />}
</div>
)}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions ui/contexts/claimSearchFilterContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as CS from 'constants/claim_search';
export const ClaimSearchFilterContext = React.createContext({
contentTypes: CS.CONTENT_TYPES,
liftUpTagSearch: false,
isChannelSearch: false,
// --Future expansion:
// durationTypes: CS.DURATION_TYPES,
// ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ type Props = {
showMature: ?boolean,
tileLayout: boolean,
orderBy?: ?string,
sortByParam?: ?string,
hideShorts?: boolean,
minDuration?: ?number,
maxDuration?: ?number,
maxAspectRatio?: ?string | ?number,
contentType?: ?string,
freshness?: ?string,
durationParam?: ?string,
customMinMinutes?: ?number,
customMaxMinutes?: ?number,
onResults?: (results: ?Array<string>) => void,
doResolveUris: (Array<string>, boolean) => void,
};
Expand All @@ -33,6 +39,12 @@ export function SearchResults(props: Props) {
doResolveUris,
maxDuration,
maxAspectRatio,
contentType,
freshness,
sortByParam,
durationParam,
customMinMinutes,
customMaxMinutes,
} = props;

const SEARCH_PAGE_SIZE = 24;
Expand All @@ -41,18 +53,76 @@ export function SearchResults(props: Props) {
const [isSearchingState, setIsSearchingState] = React.useState(false);
const isSearching = React.useRef(false);
const noMoreResults = React.useRef(false);

// Map contentType from ClaimListDiscover to lighthouse mediaType param
const mediaTypeParam = React.useMemo(() => {
if (!contentType || contentType === CS.CONTENT_ALL) return '';
// Map claim_search file types to lighthouse media types
const typeMap = {
[CS.FILE_VIDEO]: SEARCH_OPTIONS.MEDIA_VIDEO,
[CS.FILE_AUDIO]: SEARCH_OPTIONS.MEDIA_AUDIO,
[CS.FILE_IMAGE]: SEARCH_OPTIONS.MEDIA_IMAGE,
[CS.FILE_DOCUMENT]: SEARCH_OPTIONS.MEDIA_TEXT,
[CS.FILE_BINARY]: SEARCH_OPTIONS.MEDIA_APPLICATION,
[CS.FILE_MODEL]: SEARCH_OPTIONS.MEDIA_APPLICATION,
};
const mapped = typeMap[contentType];
return mapped ? `&mediaType=${mapped}` : '';
}, [contentType]);

// Map freshness to lighthouse time_filter
const timeFilterParam = React.useMemo(() => {
if (!freshness || freshness === CS.FRESH_ALL) return '';
const freshnessMap = {
[CS.FRESH_DAY]: 'today',
[CS.FRESH_WEEK]: 'thisweek',
[CS.FRESH_MONTH]: 'thismonth',
[CS.FRESH_YEAR]: 'thisyear',
};
const mapped = freshnessMap[freshness];
return mapped ? `&time_filter=${mapped}` : '';
}, [freshness]);

// Map duration filter to lighthouse min_duration/max_duration (in seconds)
const SHORT_DURATION_SECONDS = 240; // 4 minutes
const LONG_DURATION_SECONDS = 1200; // 20 minutes

const durationMinParam = React.useMemo(() => {
if (!durationParam || durationParam === CS.DURATION.ALL) return null;
if (durationParam === CS.DURATION.SHORT) return null;
if (durationParam === CS.DURATION.LONG) return LONG_DURATION_SECONDS;
if (durationParam === CS.DURATION.CUSTOM && customMinMinutes) return customMinMinutes * 60;
return null;
}, [durationParam, customMinMinutes]);

const durationMaxParam = React.useMemo(() => {
if (!durationParam || durationParam === CS.DURATION.ALL) return null;
if (durationParam === CS.DURATION.SHORT) return SHORT_DURATION_SECONDS;
if (durationParam === CS.DURATION.LONG) return null;
if (durationParam === CS.DURATION.CUSTOM && customMaxMinutes) return customMaxMinutes * 60;
return null;
}, [durationParam, customMaxMinutes]);
Comment on lines +90 to +104
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard custom duration inputs before converting to seconds.

customMinMinutes / customMaxMinutes are converted without validating finite, non-negative values, and there’s no guard for min > max. Invalid ranges can produce unstable or empty result sets unexpectedly.

Suggested fix
+  const toSeconds = (minutes) => {
+    if (minutes == null) return null;
+    if (!Number.isFinite(minutes) || minutes < 0) return null;
+    return Math.floor(minutes * 60);
+  };

   const durationMinParam = React.useMemo(() => {
     if (!durationParam || durationParam === CS.DURATION.ALL) return null;
     if (durationParam === CS.DURATION.SHORT) return null;
     if (durationParam === CS.DURATION.LONG) return LONG_DURATION_SECONDS;
-    if (durationParam === CS.DURATION.CUSTOM && customMinMinutes) return customMinMinutes * 60;
+    if (durationParam === CS.DURATION.CUSTOM) return toSeconds(customMinMinutes);
     return null;
   }, [durationParam, customMinMinutes]);

   const durationMaxParam = React.useMemo(() => {
     if (!durationParam || durationParam === CS.DURATION.ALL) return null;
     if (durationParam === CS.DURATION.SHORT) return SHORT_DURATION_SECONDS;
     if (durationParam === CS.DURATION.LONG) return null;
-    if (durationParam === CS.DURATION.CUSTOM && customMaxMinutes) return customMaxMinutes * 60;
+    if (durationParam === CS.DURATION.CUSTOM) return toSeconds(customMaxMinutes);
     return null;
   }, [durationParam, customMaxMinutes]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ui/page/claim/internal/claimPageComponent/internal/channelPage/tabs/contentTab/internal/searchResults.jsx`
around lines 90 - 104, The duration memoizers (durationMinParam and
durationMaxParam) convert customMinMinutes/customMaxMinutes to seconds without
validating values or the min>max relationship; update the React.useMemo blocks
to first validate that customMinMinutes and customMaxMinutes are finite numbers
>= 0 before converting, produce null if invalid, and enforce the range (e.g., if
computed minSeconds > maxSeconds return null or normalize/swap) so invalid or
inverted ranges don’t propagate; reference the durationMinParam and
durationMaxParam useMemo callbacks and their dependency arrays when making this
change.


// Build sort_by param: handle ascending (oldest first = ^release_time)
const isOldestFirst = sortByParam === CS.SORT_BY.OLDEST.key;
const sortBy =
!orderBy || orderBy === CS.ORDER_BY_NEW
? `&sort_by=${CS.ORDER_BY_NEW_VALUE[0]}`
? `&sort_by=${isOldestFirst ? '^' : ''}${CS.ORDER_BY_NEW_VALUE[0]}`
: orderBy === CS.ORDER_BY_TOP
? `&sort_by=${CS.ORDER_BY_TOP_VALUE[0]}`
: ``;

// Combine prop-based duration (e.g. shorts) with filter-based duration using intersection
const effectiveMinDuration =
durationMinParam && minDuration ? Math.max(durationMinParam, minDuration) : durationMinParam || minDuration || null;
const effectiveMaxDuration =
durationMaxParam && maxDuration ? Math.min(durationMaxParam, maxDuration) : durationMaxParam || maxDuration || null;

React.useEffect(() => {
noMoreResults.current = false;
setSearchResults(null);
setPage(1);
}, [searchQuery, sortBy]);
}, [searchQuery, sortBy, mediaTypeParam, timeFilterParam, effectiveMinDuration, effectiveMaxDuration]);

React.useEffect(() => {
if (onResults) {
Expand All @@ -70,16 +140,19 @@ export function SearchResults(props: Props) {
}

setIsSearchingState(true);

lighthouse
.search(
`from=${SEARCH_PAGE_SIZE * (page - 1)}` +
`&s=${encodeURIComponent(searchQuery)}` +
`&channel_id=${encodeURIComponent(claimId)}` +
sortBy +
`&nsfw=${showMature ? 'true' : 'false'}` +
(minDuration ? `&${SEARCH_OPTIONS.MIN_DURATION}=${minDuration}` : '') +
(maxDuration ? `&${SEARCH_OPTIONS.MAX_DURATION}=${maxDuration}` : '') +
(effectiveMinDuration ? `&${SEARCH_OPTIONS.MIN_DURATION}=${effectiveMinDuration}` : '') +
(effectiveMaxDuration ? `&${SEARCH_OPTIONS.MAX_DURATION}=${effectiveMaxDuration}` : '') +
`&size=${SEARCH_PAGE_SIZE}` +
mediaTypeParam +
timeFilterParam +
(maxAspectRatio ? `&${SEARCH_OPTIONS.MAX_ASPECT_RATIO}=${maxAspectRatio}` : '') +
(hideShorts ? `&${SEARCH_OPTIONS.EXCLUDE_SHORTS}=${'true'}` : '') +
(hideShorts
Expand Down Expand Up @@ -120,10 +193,12 @@ export function SearchResults(props: Props) {
showMature,
doResolveUris,
sortBy,
minDuration,
maxDuration,
effectiveMinDuration,
effectiveMaxDuration,
maxAspectRatio,
hideShorts,
mediaTypeParam,
timeFilterParam,
]);

if (!searchResults) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,24 @@ function ContentTab(props: Props) {
const [isSearching, setIsSearching] = React.useState(false);

const orderBy = urlParams.get('order');
const contentType = urlParams.get(CS.CONTENT_KEY);
const freshness = urlParams.get(CS.FRESH_KEY);
const sortByParam = urlParams.get(CS.SORT_BY_KEY);
const durationParam = urlParams.get(CS.DURATION_KEY);
const [minDurationMinutes] = usePersistedState(`minDurUserMinutes-${pathname}`, null);
const [maxDurationMinutes] = usePersistedState(`maxDurUserMinutes-${pathname}`, null);

// In Channel Page, ignore the global settings for these 2:
const [hideReposts, setHideReposts] = usePersistedState('hideRepostsChannelPage', false);
const [hideMembersOnly, setHideMembersOnly] = usePersistedState('channelPage-hideMembersOnly', false);

const isChannelSearch = searchQuery.trim().length > 2;

const claimSearchFilterCtx = {
contentTypes: CS.CONTENT_TYPES,
repost: { hideReposts, setHideReposts },
membersOnly: { hideMembersOnly, setHideMembersOnly },
isChannelSearch,
};

const claimId = claim && claim.claim_id;
Expand Down Expand Up @@ -240,6 +249,12 @@ function ContentTab(props: Props) {
tileLayout={tileLayout}
orderBy={orderBy}
hideShorts={hideShorts}
contentType={contentType}
freshness={freshness}
sortByParam={sortByParam}
durationParam={durationParam}
customMinMinutes={minDurationMinutes}
customMaxMinutes={maxDurationMinutes}
onResults={(results) => setIsSearching(results !== null)}
doResolveUris={doResolveUris}
{...(shortsOnly
Expand Down
Loading