Skip to content

feat: add predictions pagination and view more in explore#30445

Merged
juanmigdr merged 14 commits into
mainfrom
feat/add-predictions-pagination-and-view-more-in-explore
May 21, 2026
Merged

feat: add predictions pagination and view more in explore#30445
juanmigdr merged 14 commits into
mainfrom
feat/add-predictions-pagination-and-view-more-in-explore

Conversation

@juanmigdr

@juanmigdr juanmigdr commented May 20, 2026

Copy link
Copy Markdown
Member

Description

Added pagination on Explore predictions search plus the "View X more" support

Changelog

CHANGELOG entry: added pagination on Explore predictions search plus the "View X more" support

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3250

Manual testing steps

Feature: my feature name

  Scenario: user [verb for user action]
    Given [describe expected initial app state]

    When user [verb for user action]
    Then [describe expected outcome]

Screenshots/Recordings

Before

After

Screen.Recording.2026-05-20.at.15.56.51.mov

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Medium risk because it changes the searchMarkets return shape across controller/provider layers and reworks search fetching to useInfiniteQuery, which can affect Explore search results, pagination, and error handling.

Overview
Predictions search now supports pagination and server totals. PredictProvider.searchMarkets (Polymarket + controller) now returns { markets, totalResults }, plumbed from the Polymarket public-search pagination response.

Explore search consumes paged results and drives “View X more”. usePredictSearchMarketData switches to useInfiniteQuery, adds fetchMore/hasMore/totalResults, and fixes next-page logic to avoid infinite loops when client-side filtering reduces visible items; usePredictionsFeed and useExploreSearch forward pagination/total fields so header labels can compute hidden counts from server totals (and skip local Fuse re-ranking for server-ranked searches).

UI/test cleanup. Search results lists use shared searchTypes and stable item keys, and tests/mocks are updated for the new search result shape and new pagination behavior.

Reviewed by Cursor Bugbot for commit a24e1e2. Bugbot is set up for automated code reviews on this repo. Configure here.

@juanmigdr juanmigdr requested a review from a team as a code owner May 20, 2026 13:56
@juanmigdr juanmigdr enabled auto-merge May 20, 2026 14:00
Comment thread app/components/Views/TrendingView/search/viewMoreLabel.ts
Comment thread app/components/UI/Predict/hooks/usePredictSearchMarketData.tsx
juanmigdr added 2 commits May 20, 2026 16:23
…e' of github.com:MetaMask/metamask-mobile into feat/add-predictions-pagination-and-view-more-in-explore
Comment thread app/components/Views/TrendingView/feeds/predictions/usePredictionsFeed.ts Outdated
@github-actions github-actions Bot added size-XL and removed size-L labels May 20, 2026
@juanmigdr juanmigdr requested a review from a team as a code owner May 20, 2026 16:07

if (!query) {
return [];
return { markets: [], totalResults: 0 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we're adding pagination support, then searchMarkets should follow the same interface as getMarkets and return { markets: PredictMarket[], nextCursor: string | null} instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The search endpoint is a different Polymarket API (/public-search) that returns pagination.totalResults, not a cursor. Using nextCursor here would always be null and would lose the total count that drives both the "View X more" label and infinite-loop prevention in getNextPageParam. The shapes differ because the underlying APIs differ.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, I see... that's right. Forgot they didn't update that one.

Comment on lines +27 to +30
interface SearchPage {
markets: PredictMarket[];
totalResults: number;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is totalResults needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As you can see it is needed for the "View X more" label on explore search
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, total results is different than markets.length. Makes sense

public async searchMarkets(
params: SearchMarketsParams,
): Promise<PredictMarket[]> {
): Promise<{ markets: PredictMarket[]; totalResults: number }> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, let's keep the interfaces consistent

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Responded above

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bfe0ca0. Configure here.

Comment thread app/components/Views/TrendingView/search/useExploreSearch.ts
@juanmigdr juanmigdr requested a review from matallui May 21, 2026 08:20

@MarioAslau MarioAslau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM severity

M1 — V2 "View all / View X more" button is always rendered, even when there is nothing more to show

app/components/Views/TrendingView/search/ExploreSearchResultsV2.tsx

V1 gated the header link on a computed hasMore flag:

      const hasMore = !isLoading && items.length > MAX_ITEMS_PER_SECTION;
      result.push({ type: 'header', feedId, title, hasMore });
        {item.hasMore && (
          <Pressable
            onPress={() => handleViewMore(section)}
            ...
          >

V2 unconditionally pushes a header and renders a pressable "View all / View X more" link, regardless of whether the section actually has more items than MAX_ITEMS_PER_SECTION = 3:

      result.push({ type: 'header', feedId, title });
        <Pressable
          onPress={() => handleViewMore(section)}
          hitSlop={8}
          accessibilityRole="button"
          accessibilityLabel={`${getViewMoreLabel(section.feedId, section.items.length, searchQuery, section.total)} ${item.title}`}
          ...
        >
          <Text variant={TextVariant.BodyMd} color={TextColor.TextAlternative}>
            {getViewMoreLabel(
              section.feedId,
              section.items.length,
              searchQuery,
              section.total,
            )}
          </Text>
          <Icon
            name={IconName.ArrowRight}
            size={IconSize.Sm}
            color={IconColor.IconAlternative}
          />
        </Pressable>

Concrete cases this affects:

  • Local-search feeds (perps / stocks / sites) returning ≤ 3 items: V2 shows a "View all" link that navigates to the same 3 items the user already sees.
  • Predictions/tokens with serverTotal ≤ MAX_ITEMS_PER_SECTION: same — the link still appears (label collapses to "View all" via getViewMoreLabel).

getViewMoreLabel correctly degrades the text to "View all", but the button itself still renders. Either:

  • This is intentional ("View all" doubles as a tab switcher) — please confirm in the PR description / Figma, or
  • V2's flatData should compute a hasMore analogous to V1, e.g.:
    const hasMore = section.total !== undefined
      ? section.total > MAX_ITEMS_PER_SECTION
      : LOCAL_SEARCH_FEEDS.has(feedId)
        ? items.length > MAX_ITEMS_PER_SECTION
        : false;
    and gate the Pressable on it.

Why medium: this is a visible UX behavior change between V1 and V2 introduced by this PR. Worth an explicit decision and a unit test asserting the chosen behavior.


M2 — V1 still uses local items.length > 3 for hasMore; doesn't read the new server total

app/components/Views/TrendingView/search/ExploreSearchResults.tsx

V1 (which is not behind exposePagination) computes hasMore purely from the client-side array:

      const hasMore = !isLoading && items.length > MAX_ITEMS_PER_SECTION;

Today this works because useExploreSearch defaults predictionsPageSize = 20, so any predictions search with > 3 server results will load ≥ 4 items into items and trigger hasMore. But the contract is now fragile:

  • If a caller ever passes predictionsPageSize <= 3 (or upstream filtering reduces results below 4 with totalResults still large), V1 will hide the "View all" button while there are hundreds of results server-side.
  • Tokens similarly carries total: tokens.totalCount only when exposePagination is true; V1 has no visibility into that.

Recommended: have V1 use (section.total ?? items.length) > MAX_ITEMS_PER_SECTION so that when V1 is ever upgraded to use exposePagination it works correctly out of the box; and add a test exercising the "small page, large total" case.

Why medium: this is a behavioral pitfall that wouldn't surface in a code search but would silently regress "View all" visibility for predictions/tokens depending on how pageSize is tuned downstream.


M3 — Search errors are silently swallowed at the provider; the new error-path test is unreachable in production

app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts

PolymarketProvider.searchMarkets catches all errors and returns { markets: [], totalResults: 0 }:

    } catch (error) {
      DevLogger.log('Error searching markets via Polymarket API:', error);

      Logger.error(
        error instanceof Error ? error : new Error(String(error)),
        this.getErrorContext('searchMarkets', {
          hasSearchQuery: Boolean(query),
        }),
      );

      return { markets: [], totalResults: 0 };
    }

PredictController.searchMarkets wraps that call in withTrace but does not add a separate catch, so withTrace observes a successful return ({ markets: [], totalResults: 0 }) on network failure — no throw, no MARKETS_FAILED propagation.

Consequence for the new hook:

  • usePredictSearchMarketData exposes error only when the controller .searchMarkets(...) promise rejects. In production it never does.
  • The test that asserts this:
      it('sets error and clears data when search throws', async () => {
        mockSearchMarkets.mockRejectedValue(new Error('Search failed'));
        ...
        expect(result.current.error).toBe('Search failed');
    passes only because the mock rejects. The real controller path doesn't reject.

Result: when Polymarket's /public-search returns 500 or the device is offline, the search overlay shows "no results" indistinguishably from a legitimate empty query. Existing PredictFeed.view.test.tsx covers a separate "offline" UI by mocking the controller itself to reject — also non-representative of production.

This is a pre-existing concern that this PR inherits (it didn't introduce the swallow), but the PR builds a richer error contract on top of an unreachable error path. Suggest one of:

  • Remove the provider's blanket catch and let withTrace + fallbackErrorCode: MARKETS_FAILED surface the failure; or
  • Surface a sentinel field (e.g. error?: string) in { markets, totalResults } so the hook can distinguish empty from failed.

Why medium: silent failure of a network-bound search has real UX impact (users think the API "has nothing"). Worth fixing in the same PR since the contract is being modified anyway.


LOW severity

L1 — pageSize baked into the queryKey causes cache misses across contexts

    queryKey: ['predict', 'markets', 'search', trimmedQuery, pageSize],

If the explore overview uses pageSize: 20 and the predictions full-view uses, say, pageSize: 50, they will never share cache for the same query. Acceptable trade-off, just worth being aware of when tuning page sizes per surface.

L2 — No staleTime set on useInfiniteQuery

The query inherits react-query defaults (staleTime: 0), which can cause a refetch on every mount when the user navigates back to the search overlay. Consider an explicit staleTime: 30_000 (or whatever matches usePredictMarketData) to keep behavior aligned across hooks and avoid skeleton flicker.

L3 — searchMarkets trace omits page

        traceData: (result) => ({ marketCount: result.markets.length }),

For multi-page failures it would help to record page and pageSize in the trace context (and totalResults on success) for easier triage.

L4 — getNextPageParam reads lastPage.totalResults, not a stable first-page total

    getNextPageParam: (lastPage, allPages) => {
      // Compare pages fetched × pageSize against the server total rather than
      // counting client-side filtered markets, which can be fewer than the raw
      // event count and would cause infinite pagination.
      const fetched = allPages.length * pageSize;
      return fetched < lastPage.totalResults ? allPages.length + 1 : undefined;
    },

If the server total drifts between page fetches (data ingest mid-stream), hasMore can flip mid-pagination and the "View X more" label can desync with the rendered list. Low-probability for Polymarket; mention worth a comment or allPages[0].totalResults if stability matters.

L5 — V2 overview over-fetches: pageSize: 20 but only MAX_ITEMS_PER_SECTION = 3 shown

useExploreSearch defaults predictionsPageSize = 20; ExploreSearchResultsV2 slices to top 3. The extra 17 items are useful only as a prefetch for the "View all" navigation. If that navigation uses the same hook + same query + same pageSize, the cache is warm. If it ever diverges (per L1), the prefetch is wasted bandwidth. Consider documenting this rationale where predictionsPageSize is set.

L6 — Verbose accessibility label duplicating the visible text

          accessibilityLabel={`${getViewMoreLabel(section.feedId, section.items.length, searchQuery, section.total)} ${item.title}`}

This renders e.g. "View 47 more Crypto", which is fine but redundant with the visible label. Could be simplified to ${item.title}: ${getViewMoreLabel(...)} for screen reader clarity, or use accessibilityHint instead.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokePredictions, SmokeWalletPlatform, SmokeConfirmations
  • Selected Performance tags: @PerformancePredict
  • Risk Level: medium
  • AI Confidence: 88%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR introduces significant changes to the Predictions search functionality and the Trending tab's explore search system:

  1. PredictController.ts (CRITICAL): searchMarkets() return type changed from PredictMarket[] to { markets: PredictMarket[]; totalResults: number }. This is a breaking API change that propagates through the entire predictions search stack.

  2. PolymarketProvider.ts: Updated to return { markets, totalResults } with server-side pagination count from the Polymarket API's pagination.totalResults field.

  3. usePredictSearchMarketData.tsx: Migrated from useQuery to useInfiniteQuery - a significant architectural change enabling infinite scroll/pagination for search results.

  4. usePredictionsFeed.ts: Added total field, pagination support for search queries, configurable pageSize, and changed Fuse.js search behavior (skipped for server-ranked search results).

  5. useExploreSearch.ts: Refactored to support options (truncation, pagination exposure, title variants), merging V1/V2 logic into a single hook.

  6. useExploreSearchV2.ts: Simplified to delegate to useExploreSearch with exposePagination: true.

  7. ExploreSearchResults.tsx / ExploreSearchResultsV2.tsx: UI updates for search results display, style refactoring (StyleSheet instead of Tailwind), section.total instead of section.hasMore/section.totalCount.

  8. viewMoreLabel.ts: Simplified "View X more" label logic using serverTotal - predictions removed from LOCAL_SEARCH_FEEDS.

  9. tests/component-view/mocks.ts (CRITICAL): Updated mock to match new searchMarkets return type.

Tag Selection Rationale:

  • SmokePredictions: Directly affected - predictions search pagination, total results display, "View more" labels, and the entire search flow for prediction markets changed.
  • SmokeWalletPlatform: Trending tab search (ExploreSearch, ExploreSearchResults, ExploreSearchResultsV2) is the primary UI surface for these changes. The Trending tab's search functionality is core to SmokeWalletPlatform.
  • SmokeConfirmations: Required per SmokePredictions tag description - "opening/closing positions are on-chain transactions" and "When selecting SmokePredictions, also select SmokeConfirmations."

The changes are medium risk - they're well-contained within the Predictions/Trending search domain, but the API contract change in PredictController and the migration to infinite query are non-trivial changes that need E2E validation.

Performance Test Selection:
The migration from useQuery to useInfiniteQuery for predictions search, combined with changes to pagination logic and the removal of Fuse.js re-ranking for search results, could impact rendering performance of the predictions search list. The @PerformancePredict tag covers prediction market list loading and balance display, which is directly affected by these changes.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@juanmigdr

Copy link
Copy Markdown
Member Author

@cursor why was this skipped?
[Block stable-main to main PRs / Block stable-main-X.Y.Z to main (pull_request)](https://github.com/MetaMask/metamask-mobile/actions/runs/26235120680/job/77213358617?pr=30445)

@cursor

cursor Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

The "Block stable-main to main PRs" job was skipped because it's working as intended. Here's why:

Looking at the workflow file .github/workflows/block-stable-main-to-main.yml, I can see that the job has a conditional filter:

if: startsWith(github.head_ref, 'stable-main-')

Your PR is from the branch feat/add-predictions-pagination-and-view-more-in-explore, which does not start with stable-main-. Therefore, the job was automatically skipped.

What this workflow does:

This workflow is designed to block manual PRs from stable-main-X.Y.Z branches to main because those merges should only happen through automated workflows. The pattern it blocks matches: stable-main-[0-9]+\.[0-9]+\.[0-9]+ (e.g., stable-main-7.15.0).

Why it was skipped:

  • Your branch: feat/add-predictions-pagination-and-view-more-in-explore
  • Condition: Only runs if branch starts with stable-main-
  • Result: Skipped (as expected for feature branches)

This is the correct behavior - the workflow saves CI resources by only running when it needs to check stable release branches. Your feature branch PR is not affected by this restriction, so the job appropriately skipped execution.

The skipped status indicates the workflow is functioning properly, not that there's an issue with your PR.

Open in Web Open in Cursor 

@juanmigdr juanmigdr added this pull request to the merge queue May 21, 2026
Merged via the queue into main with commit 92435a2 May 21, 2026
282 of 290 checks passed
@juanmigdr juanmigdr deleted the feat/add-predictions-pagination-and-view-more-in-explore branch May 21, 2026 16:35
@github-actions github-actions Bot locked and limited conversation to collaborators May 21, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.79.0 Issue or pull request that will be included in release 7.79.0 label May 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.79.0 Issue or pull request that will be included in release 7.79.0 size-XL team-assets

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants