feat(m2): Browse — Discover, Search, Artist profile + redaction retirement - #44
Conversation
Durable tracker for screens/repos/stores/services with Jul-2026 product deltas (redaction obsolete, request→accept booking). Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
AGP 8.9.1 nags on compileSdk 36; combination builds fine — silence the sync noise until AGP catches up. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Redaction was scrapped on iOS Jul 2026 (mig 0071). Delete Redaction.kt + tests; replace moat copy with Airbnb-style trust + request→accept booking across CLAUDE/README/docs; mark F0–F2 done; fix README status. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Id-keyed hydrating ArtistsRepository and search_artists/facets SearchRepository ported from iOS; Hilt binds; unit tests for cache/filter/cursor. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Wire DiscoverViewModel (concurrent search_artists rails) into client tabs, replacing the Placeholder. Artist taps navigate to a profile stub route. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Debounced search with facet rails and pagination; artist profile hydrates full stitch + reviews; Book/Message CTAs stubbed for M3/M4. Parity checklist updated. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
|
Important Review skippedToo many files! This PR contains 113 files, which is 13 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (113)
You can disable this status message by setting the 📝 WalkthroughWalkthroughAdds the Android artist browsing flow: Supabase-backed artist, search, and review repositories; Discover, Search, and Artist Profile screens; navigation wiring; fake repositories and tests; and documentation updates for parity, booking, trust UI, and verbatim chat behavior. ChangesBrowse features
Product truth and migration documentation
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds the Android browse vertical slice and expands supporting marketplace infrastructure.
Confidence Score: 3/5The PR is not yet safe to merge because filtered search still fails against pre-0073 backend deployments. The compatibility retry preserves the new RPC parameter names with null values, so PostgREST continues resolving against the extended signature rather than falling back to the legacy search function. Files Needing Attention: app/src/main/java/in/artistant/app/data/repository/SearchRepository.kt
|
| Filename | Overview |
|---|---|
| app/src/main/java/in/artistant/app/data/repository/SearchRepository.kt | Implements RPC-backed artist search, facets, pagination, cover resolution, extended filters, and a legacy-server retry path. |
| app/src/main/java/in/artistant/app/feature/discover/DiscoverViewModel.kt | Builds and exposes the Discover feed state from the new artist repository. |
| app/src/main/java/in/artistant/app/feature/search/SearchViewModel.kt | Coordinates debounced search, filters, facets, and pagination for the Search screen. |
| app/src/main/java/in/artistant/app/feature/artist/ArtistProfileViewModel.kt | Loads artist profile data and exposes profile-screen state and actions. |
| app/src/main/java/in/artistant/app/navigation/ClientTabsScaffold.kt | Wires Discover, Search, artist profiles, and client-tab navigation together. |
Reviews (14): Last reviewed commit: "docs: flip remaining PARITY rows for sco..." | Re-trigger Greptile
Port iOS request→accept booking create (pending_confirm), accept PATCH, decline/cancel via cancel-booking EF, and gig_requests quote loop — with Fake twins, Hilt binds, and reviews insert for ReviewSheet. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Client Booking→Checkout→Confirmed (“Request sent.”) flow, Bookings tab, role-aware detail Accept/Decline, RequestQuote + ReviewSheet, wired from Artist profile Book CTA. Tick F7/F11 parity rows. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (7)
app/src/main/java/in/artistant/app/feature/search/SearchViewModel.kt (2)
111-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRedundant nested condition — simplify.
!snapshot.hasActiveQueryalready impliesquery.isBlank() && city == null && categories.isEmpty(), so the innerifis always true when the outer one is; this can be collapsed into a single check.♻️ Proposed simplification
- if (!snapshot.hasActiveQuery && snapshot.query.isBlank()) { - // Empty browse: still show top results so the tab isn't blank. - // Matching iOS empty-state rails when query+filters are empty — we - // leave results empty and show facets; only search when active. - if (snapshot.query.isBlank() && snapshot.city == null && snapshot.categories.isEmpty()) { - _state.update { - it.copy(results = emptyList(), canLoadMore = false, loadError = null, isLoading = false) - } - nextCursor = null - return - } - } + if (!snapshot.hasActiveQuery) { + // Empty browse: matching iOS empty-state rails — leave results + // empty and show facets; only search when a query/filter is active. + _state.update { + it.copy(results = emptyList(), canLoadMore = false, loadError = null, isLoading = false) + } + nextCursor = null + return + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/feature/search/SearchViewModel.kt` around lines 111 - 124, Collapse the nested condition in runSearch into a single guard using !snapshot.hasActiveQuery, preserving the existing empty-state update, nextCursor reset, and early return behavior.
125-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRaw exception message shown to the user; inconsistent with
DiscoverViewModel's friendly-copy pattern.
loadError = t.message ?: "Search failed."surfaces raw transport/RPC error text, whileDiscoverViewModel.messageFormaps the same class of errors to friendly copy. Consider reusing a similar mapping here for a consistent UX and to avoid leaking backend/RPC details.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/feature/search/SearchViewModel.kt` around lines 125 - 181, Update the exception handling in the search coroutine to map failures through the same friendly-error pattern used by DiscoverViewModel.messageFor instead of assigning t.message directly to loadError. Preserve the existing fallback behavior and stale-generation guard while ensuring backend or RPC details are not exposed to users.app/src/main/java/in/artistant/app/data/model/Artist.kt (1)
67-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRaw hex
Color(...)literals bypass the design-token system.
ArtistGradienthardcodes six palettes with raw hexColor(0xFF...)values rather than referencingAppColors. Per coding guidelines, raw hex values shouldn't be used directly in Kotlin/Compose code. If these brand gradients are meant to be content data rather than themeable UI, please confirm this is an accepted exception; otherwise consider sourcing the stops fromAppColors.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/data/model/Artist.kt` around lines 67 - 83, The ArtistGradient palettes currently use raw hex Color literals; replace those stops with the corresponding AppColors design-token references. Update only the palettes inside ArtistGradient, preserving the six palette order, gradient stop order, and palette(index) clamping behavior.Source: Coding guidelines
app/src/main/java/in/artistant/app/data/repository/ArtistsRepository.kt (1)
116-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated
artist_mediacover-resolution logic across two repositories. Both files independently queryartist_mediafiltered byartist_id/kind=photo, order by position, group by artist, and take the first row'sstoragePaththroughcoverUrl()— the same rule implemented twice.
app/src/main/java/in/artistant/app/data/repository/ArtistsRepository.kt#L116-L125: extract this query+mapping into a shared internal helper (e.g., a function taking a list of ids and returningMap<String, String>) that both repositories call.app/src/main/java/in/artistant/app/data/repository/SearchRepository.kt#L163-L183: replaceresolveCoverswith a call to the shared helper extracted fromArtistsRepository.kt, instead of re-implementing the same query.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/data/repository/ArtistsRepository.kt` around lines 116 - 125, Extract the shared artist cover query and first-cover mapping from ArtistsRepository into an internal helper accepting artist IDs and returning Map<String, String>, preserving the artist_media filters, ordering, grouping, and coverUrl() behavior. Update ArtistsRepository.kt lines 116-125 to call the helper, and replace SearchRepository.kt lines 163-183 resolveCovers implementation with the same shared helper call; both sites must use the centralized logic.app/src/main/java/in/artistant/app/navigation/ClientTabsScaffold.kt (2)
53-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer a conditional over
return@Scaffoldin thebottomBarslot.It compiles, but a non-local-looking return inside a composable slot reads as if it aborts the
Scaffoldcall. Wrapping theNavigationBarinif (showBottomBar) { ... }states the intent plainly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/navigation/ClientTabsScaffold.kt` around lines 53 - 54, Update the bottomBar slot in ClientTabsScaffold to conditionally render the existing NavigationBar only when showBottomBar is true, removing the return@Scaffold statement while preserving the current hidden and visible behavior.
80-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winHand-built
"artist/$id"duplicates the route template and skips encoding.Both call sites (Lines 81 and 87) reconstruct the path that Line 42 already declares, so the template and its usages can drift. Add one builder next to
ARTIST_PROFILE_ROUTEand encode the argument.♻️ Proposed refactor
private const val ARTIST_PROFILE_ROUTE = "artist/{artistId}" + +private fun artistProfileRoute(artistId: String): String = + "artist/${Uri.encode(artistId)}"- DiscoverScreen(onArtistClick = { id -> nav.navigate("artist/$id") }) + DiscoverScreen(onArtistClick = { id -> nav.navigate(artistProfileRoute(id)) })- SearchScreen(onArtistClick = { id -> nav.navigate("artist/$id") }) + SearchScreen(onArtistClick = { id -> nav.navigate(artistProfileRoute(id)) })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/navigation/ClientTabsScaffold.kt` around lines 80 - 98, Define a shared artist-profile route builder next to ARTIST_PROFILE_ROUTE that accepts an artist ID and URL-encodes it, then update the DiscoverScreen and SearchScreen onArtistClick navigation calls to use this builder instead of constructing "artist/$id" directly.app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt (1)
88-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Color.White/Color.Blackliterals sidestepAppColors.The scrim (Lines 62-69) and the on-image text colors are hardcoded. Even for over-photo content, these should be named tokens (e.g.
colors.onMedia,colors.scrimTop) so the locked dark-only direction stays centrally controlled.As per coding guidelines: "In Kotlin/Jetpack Compose UI, use
AppColors,AppType,Space,Size, andRadii; never use raw hex, dp, or sp values."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt` around lines 88 - 125, Replace the hardcoded Color.White and Color.Black values in ArtistTile’s scrim and on-image text styling with the appropriate AppColors tokens, such as colors.onMedia and colors.scrimTop. Use the existing theme color access pattern and preserve the current contrast, alpha values, and layout behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt`:
- Around line 45-46: Replace the raw tile dimensions and 1.dp borders with
shared AppTheme.dimens Size tokens: add the missing tile geometry and hairline
tokens in AppTheme.dimens, then use them in ArtistTile, ArtistProfileScreen, and
both affected SearchScreen sites. Update
app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt lines
45-46, app/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.kt
lines 211-215, and
app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt lines 105 and
270; preserve the existing dimensions and border behavior through the new
tokens.
In `@app/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.kt`:
- Around line 243-246: Update the review Row containing review.name and the
repeated star glyphs to expose a semantics contentDescription stating the review
rating out of five, and mark the star Text as decorative so screen readers do
not announce individual glyphs.
In `@app/src/main/java/in/artistant/app/feature/artist/ArtistProfileViewModel.kt`:
- Around line 42-60: Guard the network calls in refresh around
artistsRepository.find and artistsRepository.ensureFull with runCatching (or
equivalent exception handling), ensuring any failure updates _state with
isLoading = false and an appropriate loadError while preserving cached artist
data when available. Keep the existing successful full-artist flow and reviews
handling unchanged, and ensure exceptions cannot leave ArtistProfileViewModel
stuck loading.
In `@app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt`:
- Around line 262-275: Update the Chip composable’s Modifier chain to add button
semantics via clickable’s Role.Button and enforce the minimum touch height with
defaultMinSize using the existing AppTheme dimens Size token; preserve the
current styling, click behavior, and spacing.
- Around line 139-147: Update the snapshotFlow in LaunchedEffect(listState) so
near-end detection requires a measured, non-empty list: ensure totalItemsCount
is greater than zero and visibleItemsInfo is not empty before comparing the last
visible index. Preserve the existing threshold and loadMore() behavior after
measurement.
- Around line 162-168: Remove the redundant width = 160.dp argument from the
ArtistTile call in the search layout, keeping modifier = Modifier.weight(1f) as
the source of the tile’s width. Do not change the existing height or click
behavior.
In `@docs/FEATURE_CHECKLIST.md`:
- Around line 65-74: Update the F2 notification-permission checklist item to
list only POST_NOTIFICATIONS as completed, and explicitly state that FCM token
registration is deferred to M4. Remove the contradictory “FCM register”
completion wording while preserving the existing permission UI status and F9
dependency.
In `@README.md`:
- Around line 7-10: Update the README status banner to indicate that M2 Browse
is complete rather than stating it is next, while preserving the existing
references to the parity checklist and implementation roadmap.
---
Nitpick comments:
In `@app/src/main/java/in/artistant/app/data/model/Artist.kt`:
- Around line 67-83: The ArtistGradient palettes currently use raw hex Color
literals; replace those stops with the corresponding AppColors design-token
references. Update only the palettes inside ArtistGradient, preserving the six
palette order, gradient stop order, and palette(index) clamping behavior.
In `@app/src/main/java/in/artistant/app/data/repository/ArtistsRepository.kt`:
- Around line 116-125: Extract the shared artist cover query and first-cover
mapping from ArtistsRepository into an internal helper accepting artist IDs and
returning Map<String, String>, preserving the artist_media filters, ordering,
grouping, and coverUrl() behavior. Update ArtistsRepository.kt lines 116-125 to
call the helper, and replace SearchRepository.kt lines 163-183 resolveCovers
implementation with the same shared helper call; both sites must use the
centralized logic.
In `@app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt`:
- Around line 88-125: Replace the hardcoded Color.White and Color.Black values
in ArtistTile’s scrim and on-image text styling with the appropriate AppColors
tokens, such as colors.onMedia and colors.scrimTop. Use the existing theme color
access pattern and preserve the current contrast, alpha values, and layout
behavior.
In `@app/src/main/java/in/artistant/app/feature/search/SearchViewModel.kt`:
- Around line 111-124: Collapse the nested condition in runSearch into a single
guard using !snapshot.hasActiveQuery, preserving the existing empty-state
update, nextCursor reset, and early return behavior.
- Around line 125-181: Update the exception handling in the search coroutine to
map failures through the same friendly-error pattern used by
DiscoverViewModel.messageFor instead of assigning t.message directly to
loadError. Preserve the existing fallback behavior and stale-generation guard
while ensuring backend or RPC details are not exposed to users.
In `@app/src/main/java/in/artistant/app/navigation/ClientTabsScaffold.kt`:
- Around line 53-54: Update the bottomBar slot in ClientTabsScaffold to
conditionally render the existing NavigationBar only when showBottomBar is true,
removing the return@Scaffold statement while preserving the current hidden and
visible behavior.
- Around line 80-98: Define a shared artist-profile route builder next to
ARTIST_PROFILE_ROUTE that accepts an artist ID and URL-encodes it, then update
the DiscoverScreen and SearchScreen onArtistClick navigation calls to use this
builder instead of constructing "artist/$id" directly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 57522390-4593-45ee-869d-8505d3bdd76f
📒 Files selected for processing (34)
CLAUDE.mdREADME.mdapp/src/main/java/in/artistant/app/core/di/RepositoryModule.ktapp/src/main/java/in/artistant/app/data/model/Artist.ktapp/src/main/java/in/artistant/app/data/model/SearchTypes.ktapp/src/main/java/in/artistant/app/data/repository/ArtistsRepository.ktapp/src/main/java/in/artistant/app/data/repository/FakeArtistsRepository.ktapp/src/main/java/in/artistant/app/data/repository/FakeSearchRepository.ktapp/src/main/java/in/artistant/app/data/repository/ReviewsRepository.ktapp/src/main/java/in/artistant/app/data/repository/SearchRepository.ktapp/src/main/java/in/artistant/app/designsystem/component/ArtistTile.ktapp/src/main/java/in/artistant/app/designsystem/component/EmptyState.ktapp/src/main/java/in/artistant/app/domain/chat/Redaction.ktapp/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.ktapp/src/main/java/in/artistant/app/feature/artist/ArtistProfileViewModel.ktapp/src/main/java/in/artistant/app/feature/discover/DiscoverScreen.ktapp/src/main/java/in/artistant/app/feature/discover/DiscoverViewModel.ktapp/src/main/java/in/artistant/app/feature/search/SearchScreen.ktapp/src/main/java/in/artistant/app/feature/search/SearchViewModel.ktapp/src/main/java/in/artistant/app/navigation/ClientTabsScaffold.ktapp/src/test/java/in/artistant/app/data/repository/BrowseRepositoryTest.ktapp/src/test/java/in/artistant/app/domain/chat/RedactionTest.ktapp/src/test/java/in/artistant/app/feature/discover/DiscoverFeedLogicTest.ktapp/src/test/java/in/artistant/app/feature/search/SearchViewModelLogicTest.ktdocs/ANDROID_MIGRATION_PLAN.mddocs/API_MAPPING.mddocs/ARCHITECTURE.mddocs/FEATURE_CHECKLIST.mddocs/IMPLEMENTATION_ROADMAP.mddocs/PARITY_CHECKLIST.mddocs/PROJECT_STRUCTURE.mddocs/RISKS_AND_DECISIONS.mddocs/SCREEN_INVENTORY.mdgradle.properties
💤 Files with no reviewable changes (2)
- app/src/test/java/in/artistant/app/domain/chat/RedactionTest.kt
- app/src/main/java/in/artistant/app/domain/chat/Redaction.kt
| width: Dp = 192.dp, | ||
| height: Dp = 252.dp, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Raw dp literals across the new browse UI bypass the design-token system. Four sites hardcode dimensions instead of reading Size/Radii tokens; the shared root cause is missing hairline and tile-geometry tokens in AppTheme.dimens. Add them once, then reference them everywhere.
app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt#L45-L46: replace the192.dp/252.dpparameter defaults withAppTheme.dimens.sizetile tokens.app/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.kt#L211-L215: replace thewidth = 1.dpborder with a hairlineSizetoken.app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt#L105-L105: replace the.height(1.dp)divider with the same hairline token.app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt#L270-L270: replace the1.dpchip border width with the same hairline token.
As per coding guidelines: "In Kotlin/Jetpack Compose UI, use AppColors, AppType, Space, Size, and Radii; never use raw hex, dp, or sp values."
📍 Affects 3 files
app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt#L45-L46(this comment)app/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.kt#L211-L215app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt#L105-L105app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt#L270-L270
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt`
around lines 45 - 46, Replace the raw tile dimensions and 1.dp borders with
shared AppTheme.dimens Size tokens: add the missing tile geometry and hairline
tokens in AppTheme.dimens, then use them in ArtistTile, ArtistProfileScreen, and
both affected SearchScreen sites. Update
app/src/main/java/in/artistant/app/designsystem/component/ArtistTile.kt lines
45-46, app/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.kt
lines 211-215, and
app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt lines 105 and
270; preserve the existing dimensions and border behavior through the new
tokens.
Source: Coding guidelines
| Row(horizontalArrangement = Arrangement.spacedBy(space.sm)) { | ||
| Text(review.name, style = AppTheme.type.callout, color = colors.ink) | ||
| Text("★".repeat(review.rating.coerceIn(0, 5)), style = AppTheme.type.caption, color = colors.warm) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Star glyph rating is not screen-reader accessible.
"★".repeat(...) announces as a run of star characters. Add a semantics description on the Row (e.g. Modifier.semantics { contentDescription = "Rated ${review.rating} out of 5" }) and mark the glyph Text as decorative.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/in/artistant/app/feature/artist/ArtistProfileScreen.kt`
around lines 243 - 246, Update the review Row containing review.name and the
repeated star glyphs to expose a semantics contentDescription stating the review
rating out of five, and mark the star Text as decorative so screen readers do
not announce individual glyphs.
| fun refresh() { | ||
| viewModelScope.launch { | ||
| _state.update { it.copy(isLoading = true, loadError = null) } | ||
| val cached = artistsRepository.find(artistId) | ||
| if (cached != null) { | ||
| _state.update { it.copy(artist = cached) } | ||
| } | ||
| val full = artistsRepository.ensureFull(artistId) | ||
| if (full == null) { | ||
| _state.update { | ||
| it.copy( | ||
| isLoading = false, | ||
| loadError = if (it.artist == null) "Artist not found." else null, | ||
| ) | ||
| } | ||
| return@launch | ||
| } | ||
| val popularIdx = full.packages.indexOfFirst { it.popular }.takeIf { it >= 0 } ?: 0 | ||
| val reviews = runCatching { reviewsRepository.listForArtist(artistId) }.getOrDefault(emptyList()) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unhandled exceptions from find/ensureFull leave the screen stuck on the spinner.
Lines 45 and 49 are unguarded network calls. If either throws, the coroutine dies before any isLoading = false update, so ArtistProfileScreen renders the indefinite CircularProgressIndicator branch with no retry path. The reviews call on Line 60 already uses runCatching; apply the same treatment to the artist fetches.
🛡️ Proposed fix
_state.update { it.copy(isLoading = true, loadError = null) }
- val cached = artistsRepository.find(artistId)
+ val cached = runCatching { artistsRepository.find(artistId) }.getOrNull()
if (cached != null) {
_state.update { it.copy(artist = cached) }
}
- val full = artistsRepository.ensureFull(artistId)
- if (full == null) {
+ val fullResult = runCatching { artistsRepository.ensureFull(artistId) }
+ val full = fullResult.getOrNull()
+ if (full == null) {
_state.update {
it.copy(
isLoading = false,
- loadError = if (it.artist == null) "Artist not found." else null,
+ loadError = when {
+ it.artist != null -> null
+ fullResult.isFailure -> "Couldn't load this artist. Try again."
+ else -> "Artist not found."
+ },
)
}
return@launch
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun refresh() { | |
| viewModelScope.launch { | |
| _state.update { it.copy(isLoading = true, loadError = null) } | |
| val cached = artistsRepository.find(artistId) | |
| if (cached != null) { | |
| _state.update { it.copy(artist = cached) } | |
| } | |
| val full = artistsRepository.ensureFull(artistId) | |
| if (full == null) { | |
| _state.update { | |
| it.copy( | |
| isLoading = false, | |
| loadError = if (it.artist == null) "Artist not found." else null, | |
| ) | |
| } | |
| return@launch | |
| } | |
| val popularIdx = full.packages.indexOfFirst { it.popular }.takeIf { it >= 0 } ?: 0 | |
| val reviews = runCatching { reviewsRepository.listForArtist(artistId) }.getOrDefault(emptyList()) | |
| fun refresh() { | |
| viewModelScope.launch { | |
| _state.update { it.copy(isLoading = true, loadError = null) } | |
| val cached = runCatching { artistsRepository.find(artistId) }.getOrNull() | |
| if (cached != null) { | |
| _state.update { it.copy(artist = cached) } | |
| } | |
| val fullResult = runCatching { artistsRepository.ensureFull(artistId) } | |
| val full = fullResult.getOrNull() | |
| if (full == null) { | |
| _state.update { | |
| it.copy( | |
| isLoading = false, | |
| loadError = when { | |
| it.artist != null -> null | |
| fullResult.isFailure -> "Couldn't load this artist. Try again." | |
| else -> "Artist not found." | |
| }, | |
| ) | |
| } | |
| return@launch | |
| } | |
| val popularIdx = full.packages.indexOfFirst { it.popular }.takeIf { it >= 0 } ?: 0 | |
| val reviews = runCatching { reviewsRepository.listForArtist(artistId) }.getOrDefault(emptyList()) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/in/artistant/app/feature/artist/ArtistProfileViewModel.kt`
around lines 42 - 60, Guard the network calls in refresh around
artistsRepository.find and artistsRepository.ensureFull with runCatching (or
equivalent exception handling), ensuring any failure updates _state with
isLoading = false and an appropriate loadError while preserving cached artist
data when available. Keep the existing successful full-artist flow and reviews
handling unchanged, and ensure exceptions cannot leave ArtistProfileViewModel
stuck loading.
| LaunchedEffect(listState) { | ||
| snapshotFlow { | ||
| val info = listState.layoutInfo | ||
| val last = info.visibleItemsInfo.lastOrNull()?.index ?: 0 | ||
| last >= info.totalItemsCount - 3 | ||
| } | ||
| .distinctUntilChanged() | ||
| .collect { nearEnd -> if (nearEnd) viewModel.loadMore() } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Pagination fires before the list has been measured.
On the first frame totalItemsCount is 0 and visibleItemsInfo is empty, so the expression evaluates 0 >= -3 → true and loadMore() is called immediately for a list that was never scrolled. Gate on a measured, non-empty list.
🐛 Proposed fix
snapshotFlow {
val info = listState.layoutInfo
- val last = info.visibleItemsInfo.lastOrNull()?.index ?: 0
- last >= info.totalItemsCount - 3
+ val last = info.visibleItemsInfo.lastOrNull()?.index ?: return@snapshotFlow false
+ info.totalItemsCount > 0 && last >= info.totalItemsCount - 3
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| LaunchedEffect(listState) { | |
| snapshotFlow { | |
| val info = listState.layoutInfo | |
| val last = info.visibleItemsInfo.lastOrNull()?.index ?: 0 | |
| last >= info.totalItemsCount - 3 | |
| } | |
| .distinctUntilChanged() | |
| .collect { nearEnd -> if (nearEnd) viewModel.loadMore() } | |
| } | |
| LaunchedEffect(listState) { | |
| snapshotFlow { | |
| val info = listState.layoutInfo | |
| val last = info.visibleItemsInfo.lastOrNull()?.index ?: return@snapshotFlow false | |
| info.totalItemsCount > 0 && last >= info.totalItemsCount - 3 | |
| } | |
| .distinctUntilChanged() | |
| .collect { nearEnd -> if (nearEnd) viewModel.loadMore() } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt` around
lines 139 - 147, Update the snapshotFlow in LaunchedEffect(listState) so
near-end detection requires a measured, non-empty list: ensure totalItemsCount
is greater than zero and visibleItemsInfo is not empty before comparing the last
visible index. Preserve the existing threshold and loadMore() behavior after
measurement.
| ArtistTile( | ||
| artist = artist, | ||
| onClick = { onArtistClick(artist.id) }, | ||
| modifier = Modifier.weight(1f), | ||
| width = 160.dp, | ||
| height = 220.dp, | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Modifier.weight(1f) and width = 160.dp contradict each other.
weight hands ArtistTile an exact width constraint, so the .width(160.dp) applied inside the tile (ArtistTile.kt Line 52) is coerced away. The literal is dead and misleading — drop it and let the weight drive the width.
♻️ Proposed fix
ArtistTile(
artist = artist,
onClick = { onArtistClick(artist.id) },
modifier = Modifier.weight(1f),
- width = 160.dp,
height = 220.dp,
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ArtistTile( | |
| artist = artist, | |
| onClick = { onArtistClick(artist.id) }, | |
| modifier = Modifier.weight(1f), | |
| width = 160.dp, | |
| height = 220.dp, | |
| ) | |
| ArtistTile( | |
| artist = artist, | |
| onClick = { onArtistClick(artist.id) }, | |
| modifier = Modifier.weight(1f), | |
| height = 220.dp, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt` around
lines 162 - 168, Remove the redundant width = 160.dp argument from the
ArtistTile call in the search layout, keeping modifier = Modifier.weight(1f) as
the source of the tile’s width. Do not change the existing height or click
behavior.
| private fun Chip(label: String, selected: Boolean, onClick: () -> Unit) { | ||
| val colors = AppTheme.colors | ||
| val space = AppTheme.dimens.space | ||
| Text( | ||
| text = label, | ||
| style = AppTheme.type.caption, | ||
| color = if (selected) colors.brandInk else colors.ink2, | ||
| modifier = Modifier | ||
| .border(1.dp, if (selected) colors.brand else colors.line, RoundedCornerShape(AppTheme.dimens.radii.sm)) | ||
| .background(if (selected) colors.brand else colors.bg) | ||
| .clickable(onClick = onClick) | ||
| .padding(horizontal = space.md, vertical = space.sm), | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Chip lacks button semantics and a minimum touch target.
A bare clickable Text announces as text, not a control, and the caption-sized chip can fall below the 48dp target. Add role = Role.Button to clickable and Modifier.defaultMinSize(minHeight = ...) from a Size token.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/in/artistant/app/feature/search/SearchScreen.kt` around
lines 262 - 275, Update the Chip composable’s Modifier chain to add button
semantics via clickable’s Role.Button and enforce the minimum touch height with
defaultMinSize using the existing AppTheme dimens Size token; preserve the
current styling, click behavior, and spacing.
Source: Coding guidelines
Role-aware Accept/Decline surfaces for artists; client profile Request a quote CTA; ArtistTabsScaffold wired for Home/Gigs + detail routes. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
… banner Explicit-column messages repo (no redaction), find-or-create thread from Artist profile, Airbnb-style safety banner. Realtime subscribe + FCM deferred. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
AccountRepository wraps delete-account + data-export EFs; ProfileScreen on client tab and artist Home Settings; legal URL hooks in AppEnvironment. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Eleven-step wizard matching iOS order; Publish upserts artists + setup_complete; incomplete artists route to wizard; EPK tab read-only shell. CameraX media upload and packages/samples table writes deferred. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
PaywallScreen + EntitlementStore compile and navigate when the flag is flipped; default off so v1 matchmaker behaviour is unchanged. Docs tick. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Point the session bootstrap at the open parity PR: booking request→accept, messages without redaction, wizard/EPK scaffold, profile DPDP, inert paywall. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Progress update (same branch)Beyond the original M2 Browse scope, this PR now also carries substantial M3–M7 parity work (still compile + unit-test verified only — no emulator here):
Gate: |
Wire postgres INSERT channel per open thread, collapse send/RETURNING/Realtime echo races, and add tap-to-retry delivery states — matching iOS MessageStore. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Register FCM via claim_device_token (0069/0075), route artistant_* payloads into pending thread/booking/gig channels, and wire both role scaffolds to consume them. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Port saved_artists optimistic hearts and the accordion SearchFilterSheet with price_histogram + 0073 filter dims; wire profile save and filter badge. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Finish the publish path beyond the scaffold: replace_packages / replace_tech_rider / published=true sync, gallery+SAF picks staged in WizardMediaCache, and serial UploadQueue for cover/samples after go-live. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Artist availability chips write days_available / default_time_slots with a seed-failure guard; Profile toggles CalendarContract mirroring of confirmed gigs (owner-gated map, planner unit-tested). Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
ScoreRepository reads artists metric_* columns + score_history; self explainer from Home, client breakdown sheet from profile score chip. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
EPK writes packages/tech/links/samples; chat Details sheet reports via public.reports with local soft-fail log when the table is absent. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Update PARITY_CHECKLIST + FEATURE_CHECKLIST + CLAUDE current-state after the M5/M6 parity wave on feature/m2-browse. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Shared Canvas ring with New-tier nil handling, history sparkline + delta sheet, and Home/profile wiring so Bookability Score matches iOS presentation. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Persist upload snapshots across process death, kick drains via WorkManager, add TakePicture cover capture + Media3 ≤10s trim so wizard media survives kills. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
EPK photo grid + reorder_artist_media; gig-request clash card from busy days; Profile calendar picker among writable calendars. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Wire BillingClient behind subscriptionsEnabled; tick FEATURE/PARITY/CLAUDE for the end-to-end polish wave. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
Keep our M2–M8 ports (request→accept, verbatim chat, UploadQueue, PlayBilling). Take additive main wins: #12 OAuth deepLinkError surface, DeepLinkRouter/P2b push channels, PostHog/Sentry no-op wrappers, design extras. Drop main's parallel feature ports that conflicted. Co-Authored-By: Cursor Grok 4.5 <noreply@cursor.com>
|
Too many files changed for review. ( Bypass the limit by tagging |
Summary
Redaction.kt); docs/roadmap/checklists updated for Airbnb trust + request→accept booking. Tick F0–F2.docs/PARITY_CHECKLIST.mdmarks Discover/Search/Artist profile done.Test plan
./gradlew :app:assembleDebuggreen./gradlew :app:testDebugUnitTestgreenFollow-ups
Closes the M2 Browse milestone (see
docs/IMPLEMENTATION_ROADMAP.md).Made with Cursor
Summary by CodeRabbit
New Features
Documentation
Chores