Skip to content

DISC-386 && Disc-388 && Disc-392: Open Calls filter Network layer - #2552

Merged
Arkariang merged 3 commits into
masterfrom
imartin/DISC-386-388-392
Jul 30, 2026
Merged

DISC-386 && Disc-388 && Disc-392: Open Calls filter Network layer#2552
Arkariang merged 3 commits into
masterfrom
imartin/DISC-386-388-392

Conversation

@Arkariang

@Arkariang Arkariang commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📲 What
Adds the network + ViewModel + feature-gate foundation for the Open Calls (Creative Prompt) search filter:

  • New GetCreativePromptTags GraphQL query and a Tag model (id/name/url/slug).
  • KSApolloClientV2.getTags() to fetch Creative Prompt tags.
  • tagId threaded into the FetchProjects query and DiscoveryParams, so a selected tag scopes search results.
  • FilterMenuViewModel.getTags() exposes the tag list via FilterMenuUIState.tagsList, loaded on screen entry.
  • SearchAndFilterViewModel.isOpenCallsEnabled, gated on the new android_open_calls Statsig gate.

🤔 Why
Open Calls lets backers filter search to projects participating in a Creative Prompt (Make 100, Zine Quest, etc.). This is the first of the chained PRs — it lands the data/gate layer so the UI PR can build on top without a giant single diff.

🛠 How

  • tags.graphql: new query scoped to CREATIVE_PROMPT. Tag.id is a Long decoded from the Relay ID!; converted with id.toInt() at the DiscoveryParams.tagId (Int) boundary.
  • project.graphql: added $tagId: Int variable, passed to projects(tagId:).
  • ViewModel: getTags() fetches on SearchAndFilterActivity entry and emits into FilterMenuUIState. applyParams(...) gained a tagId argument.
  • Gate read follows the established configReady + EvalReason.Unrecognized guard pattern. This SearchAndFilterViewModel.isOpenCallsEnabled will be consumed to hide/show the UI piece in follow up PR's.

👀 See
Screenshot 2026-07-28 at 12 01 46 PM

📋 QA
No user-facing UI yet, but use Network Profiler to check the newly added query response. Unit test coverage added in FilterMenuViewModelTest and SearchAndFilterViewModelTest.

Story 📖
DISC-386
DISC-388
DISC-392

Arkariang and others added 2 commits July 27, 2026 13:47
Adds the backend plumbing for the Open Calls (Creative Prompt tag) search
filter, ahead of the UI and deep-link PRs. Gated behind the new
StatsigGateKey.ANDROID_OPEN_CALLS.

Network / data:
- New Tag model (Parcelable, id decoded from Relay ID via decodeRelayId).
- New GetCreativePromptTags GraphQL query (tags scoped to CREATIVE_PROMPT).
- FetchProjects gains a $tagId: Int variable; buildFetchProjectsQuery()
  forwards DiscoveryParams.tagId() via a new T?.toOptional() helper.
- getTags(): Result<List<Tag>> on ApolloClientTypeV2 + KSApolloClientV2
  (mirrors getCategories()), plus MockApolloClientV2 override and TagFactory.

ViewModel / gate:
- FilterMenuViewModel owns a tagsList via a getTags() loader mirroring
  getRootCategories(); FilterMenuUIState gains tagsList.
- SearchAndFilterViewModel.updateParamsToSearchWith() gains tagId: Int?,
  applied through DiscoveryParams.Builder.tagId().
- isOpenCallsEnabled StateFlow derived from ANDROID_OPEN_CALLS using the
  configReady + EvalReason.Unrecognized pattern (mirrors
  isVideoFeedBannerVisible), not the one-shot isReady.

Tests: getTags success/error, updateParamsToSearchWith(tagId), and gate
on/off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds network + viewmodel support for an “Open Calls / Creative Prompt” filter in Search, including fetching Creative Prompt tags, passing a tagId into the projects search query, and gating UI exposure via Statsig.

Changes:

  • Extend FetchProjects GraphQL query + KSApolloClientV2 to accept/send an optional tagId discovery parameter.
  • Add a new Tag model plus getTags() API (GraphQL GetCreativePromptTags) and surface tags via FilterMenuViewModel.
  • Add Statsig gate ANDROID_OPEN_CALLS and expose it via SearchAndFilterViewModel; add/extend unit tests for the new behaviors.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
app/src/test/java/com/kickstarter/features/search/viewmodel/SearchAndFilterViewModelTest.kt Adds test coverage for tagId search param updates and the Open Calls Statsig gate state.
app/src/test/java/com/kickstarter/features/search/viewmodel/FilterMenuViewModelTest.kt Adds tests for getTags() success/error flows and UI state updates.
app/src/main/java/com/kickstarter/services/KSApolloClientV2.kt Adds getTags() implementation and wires tagId into FetchProjectsQuery variables.
app/src/main/java/com/kickstarter/models/Tag.kt Introduces a Parcelable Tag model (id/name/url/slug) for Creative Prompt tags.
app/src/main/java/com/kickstarter/mock/services/MockApolloClientV2.kt Extends mock client to support getTags() for tests.
app/src/main/java/com/kickstarter/mock/factories/TagFactory.kt Adds factory helpers for generating tag fixtures in tests.
app/src/main/java/com/kickstarter/libs/featureflag/StatsigClient.kt Adds ANDROID_OPEN_CALLS gate key.
app/src/main/java/com/kickstarter/features/search/viewmodel/SearchAndFilterViewModel.kt Adds isOpenCallsEnabled StateFlow and tagId support in updateParamsToSearchWith.
app/src/main/java/com/kickstarter/features/search/viewmodel/FilterMenuViewModel.kt Adds tagsList to UI state and a getTags() fetch method.
app/src/main/java/com/kickstarter/features/search/ui/SearchAndFilterActivity.kt Triggers tag fetch on activity creation.
app/src/main/graphql/tags.graphql Adds GetCreativePromptTags query.
app/src/main/graphql/project.graphql Adds $tagId variable and tagId argument to FetchProjects query.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 38 to 40
filterMenuViewModel.getRootCategories()
filterMenuViewModel.getTags()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow up PR wil hide/show the UI using the gate, can comment out this piece, is as of now more to facilitate QA.

* Collapses the repeated `if (x == null) Optional.absent() else Optional.present(x)` pattern
* used when building Apollo query variables.
*/
private fun <T : Any> T?.toOptional(): Optional<T> =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This could be applied broadly on this entire file, will do so on follow up PR if you agree @tonyteate

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.

Nice! Yep, honestly surprised this isn't built-in.

@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 73.43750% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.71%. Comparing base (b6fa410) to head (f5ef6e6).

Files with missing lines Patch % Lines
app/src/main/java/com/kickstarter/models/Tag.kt 66.66% 12 Missing ⚠️
...r/features/search/viewmodel/FilterMenuViewModel.kt 73.33% 0 Missing and 4 partials ⚠️
...tures/search/viewmodel/SearchAndFilterViewModel.kt 91.66% 0 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #2552      +/-   ##
============================================
+ Coverage     65.69%   65.71%   +0.01%     
- Complexity     2558     2563       +5     
============================================
  Files           402      403       +1     
  Lines         32098    32159      +61     
  Branches       4562     4569       +7     
============================================
+ Hits          21088    21133      +45     
- Misses         8595     8606      +11     
- Partials       2415     2420       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Arkariang
Arkariang marked this pull request as ready for review July 28, 2026 20:19
@Arkariang
Arkariang requested a review from tonyteate July 28, 2026 20:20
}

scope.launch {
statsigClient.configReady.collect { configReady ->

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.

Do we want to collect configReady here continuously? Could useful to do a filter or one-time check e.g. statsigClient.configReady.first { it }.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I do not expect this piece to be updated unless a change on the internal statsig user state, or changes on the rollout state. I wanna avoid the flickering we saw with VideoFeed, granted I could add some logging to verify once released really how many times this is called.
Given this specific new filter is not highly visible I could see too collecting just once, maybe I'm playing overly safe here.

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.

Gotcha! Would be curious to see the logging if you end up doing it. The previous case seemed like a special case, but if we have to re-use this pattern, we might want to revisit the custom cache key. Ideally, if I'm understanding the position of the documentation, accessing Statsig should be "transparent" -- we can always get a value, change the UX based on the value, but should not have to think about when or where we get that value.

fun slug() = this.slug

@Parcelize
data class Builder(

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.

  • With Tag as a data class with a public constructor, is the Builder still necessary?
  • If we keep the Builder, I'd argue that it should not be Parcelable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Noted, Will change to a data class on follow up PR!

.id(1L)
.name("Make 100")
.slug("make-100")
.url("https://www.kickstarter.com/discover/advanced?tag_id=1")

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.

Super-nit for clarity: I think the urls here are of the form /discover/tags/<tags> (though I know it's tag_id when used a search param)

@tonyteate tonyteate 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.

Looks good! Please see this comment for possible follow-up.

@Arkariang
Arkariang merged commit 1ae55d5 into master Jul 30, 2026
3 checks passed
@Arkariang
Arkariang deleted the imartin/DISC-386-388-392 branch July 30, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants