Feat/merge market pages - #6517
Conversation
There was a problem hiding this comment.
Pull request overview
Merges crypto, perpetual, stock, watchlist, and indicator markets into a unified Compose page.
Changes:
- Adds unified market models, filtering, sorting, settings, and tests.
- Integrates live market, favorite, indicator, and perpetual data.
- Centralizes Gradle dependency and plugin versions.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
settings.gradle.kts |
Centralizes plugin versions. |
build.gradle.kts |
Consolidates dependency versions. |
app/build.gradle.kts |
Uses centralized versions. |
MarketPageModelsTest.kt |
Tests market mapping and sorting. |
strings.xml |
Adds market labels. |
values-zh-rTW/strings.xml |
Adds Traditional Chinese labels. |
values-zh-rCN/strings.xml |
Adds Simplified Chinese labels. |
ic_config.xml |
Adds display-settings icon. |
MultiColorProgressBar.kt |
Supports custom segment colors. |
SwapViewModel.kt |
Routes market access through repository. |
MarketFragment.kt |
Hosts the new Compose market page. |
MarketPageViewModel.kt |
Manages unified market state and refreshes. |
MarketPageModels.kt |
Defines market entries and mapping logic. |
MarketPage.kt |
Implements the unified market UI. |
TokenRepository.kt |
Adds market fetching and favorite observation. |
MarketDao.kt |
Adds reactive favorite-market query. |
Comments suppressed due to low confidence (3)
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:162
- This clickable scanner icon has no accessibility label, so screen readers announce an unlabeled button.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:460
- The favorite control exposes neither a label nor its selected state, so assistive technology cannot identify whether activating it will add or remove the market. Give it a localized action label and toggle/selected semantics based on
entry.isFavored.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:911
- The dialog's close button is unlabeled for screen-reader users.
contentDescription = null,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (oldSettings.priceChangePeriod != settings.priceChangePeriod) { | ||
| refreshMarkets() | ||
| } |
| RxBus.listen(GlobalMarketEvent::class.java) | ||
| .observeOn(AndroidSchedulers.mainThread()) | ||
| .autoDispose(destroyScope) | ||
| .subscribe { _ -> | ||
| marketsAdapter.notifyDataSetChanged() | ||
| watchlistAdapter.notifyDataSetChanged() | ||
| } | ||
| bindData() | ||
| view.viewTreeObserver.addOnGlobalLayoutListener { | ||
| if (view.isShown) { | ||
| if (job?.isActive == true) return@addOnGlobalLayoutListener | ||
| job = lifecycleScope.launch { | ||
| delay(30000) | ||
| updateUI() | ||
| } | ||
| } else { | ||
| job?.cancel() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun loadGlobalMarket() { | ||
| try { | ||
| defaultSharedPreferences.getString(PREF_GLOBAL_MARKET, null)?.let { json -> | ||
| GsonHelper.customGson.fromJson(json, GlobalMarket::class.java)?.let { | ||
| binding.apply { | ||
| marketCap.render(R.string.Global_Market_Cap, it.marketCap, BigDecimal(it.marketCapChangePercentage)) | ||
| volume.render(R.string.volume_24h, it.volume, BigDecimal(it.volumeChangePercentage)) | ||
| dominance.render(R.string.Dominance, BigDecimal(it.dominancePercentage), it.dominance) | ||
| } | ||
| } | ||
| } | ||
| } catch (e: Exception) { | ||
| Timber.e(e) | ||
| } | ||
| } | ||
|
|
||
| private var type = MixinApplication.appContext.defaultSharedPreferences.getInt(Constants.Account.PREF_MARKET_TYPE, TYPE_ALL) | ||
| set(value) { | ||
| if (field != value) { | ||
| field = value | ||
| defaultSharedPreferences.putInt(Constants.Account.PREF_MARKET_TYPE, value) | ||
| when (type) { | ||
| TYPE_ALL -> { | ||
| binding.dropTopSort.isVisible = true | ||
| binding.titleLayout.setText(R.string.Market_Cap) | ||
| binding.markets.isVisible = true | ||
| binding.watchlist.isVisible = false | ||
| binding.titleLayout.isVisible = true | ||
| binding.empty.isVisible = false | ||
| } | ||
|
|
||
| else -> { | ||
| binding.dropTopSort.isVisible = false | ||
| binding.titleLayout.setText(R.string.Watchlist) | ||
| binding.markets.isVisible = false | ||
| if (watchlistAdapter.itemCount == 0) { | ||
| binding.titleLayout.isVisible = false | ||
| binding.empty.isVisible = true | ||
| binding.watchlist.isVisible = false | ||
| } else { | ||
| binding.titleLayout.isVisible = true | ||
| binding.empty.isVisible = false | ||
| binding.watchlist.isVisible = true | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private var top = 0 // 0 is top100, 1 is top200, 2 is top500 | ||
| set(value) { | ||
| if (field != value) { | ||
| field = value | ||
| bindData() | ||
| } | ||
| } | ||
|
|
||
| private var lastFiatCurrency: String? = null | ||
|
|
||
| private var currentOrder: MarketSort = MarketSort.RANK_ASCENDING | ||
|
|
||
| private var marketJob: Job? = null | ||
| private var watchlistJob: Job? = null | ||
| private var loadStateJob: Job? = null | ||
|
|
||
| @SuppressLint("NotifyDataSetChanged") | ||
| private fun bindData() { | ||
| val limit = when (top) { | ||
| 1 -> 200 | ||
| 2 -> 500 | ||
| else -> 100 | ||
| } | ||
|
|
||
| binding.dropTopTv.text = getString( | ||
| R.string.top_count, | ||
| when (top) { | ||
| 1 -> 200 | ||
| 2 -> 500 | ||
| else -> 100 | ||
| } | ||
| ) | ||
|
|
||
| binding.dropPercentageTv.text = if (topPercentage == 0) { | ||
| getString(R.string.change_percent_period_day, 7) | ||
| } else { | ||
| getString(R.string.change_percent_period_hour, 24) | ||
| } | ||
|
|
||
| // Cancel previous job if it exists | ||
| marketJob?.cancel() | ||
| watchlistJob?.cancel() | ||
| loadStateJob?.cancel() | ||
|
|
||
| marketJob = viewLifecycleOwner.lifecycleScope.launch { | ||
| walletViewModel.getWeb3Markets(limit, currentOrder).collectLatest { pagingData -> | ||
| marketsAdapter.submitData(pagingData) | ||
| if (lastFiatCurrency != Session.getFiatCurrency()) { | ||
| lastFiatCurrency = Session.getFiatCurrency() | ||
| marketsAdapter.notifyDataSetChanged() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| watchlistJob = viewLifecycleOwner.lifecycleScope.launch { | ||
| walletViewModel.getFavoredWeb3Markets(currentOrder).collectLatest { pagingData -> | ||
| watchlistAdapter.submitData(pagingData) | ||
| if (lastFiatCurrency != Session.getFiatCurrency()) { | ||
| lastFiatCurrency = Session.getFiatCurrency() | ||
| watchlistAdapter.notifyDataSetChanged() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| loadStateJob = viewLifecycleOwner.lifecycleScope.launch { | ||
| watchlistAdapter.loadStateFlow.collectLatest { _ -> | ||
| val isEmpty = watchlistAdapter.itemCount == 0 | ||
| if (isEmpty && type == TYPE_FOV) { | ||
| binding.titleLayout.isVisible = false | ||
| binding.empty.isVisible = true | ||
| binding.watchlist.isVisible = false | ||
| } else if (type == TYPE_FOV) { | ||
| binding.titleLayout.isVisible = true | ||
| binding.empty.isVisible = false | ||
| binding.watchlist.isVisible = true | ||
| } | ||
| } | ||
| } | ||
| .subscribe { viewModel.loadIndicator() } |
| IconButton(onClick = onSearch) { | ||
| Icon( | ||
| painter = painterResource(R.drawable.ic_search_home), | ||
| contentDescription = null, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (4)
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:155
- This actionable search button has no accessible name, so screen readers announce an unlabeled control.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:460
- The favorite toggle is an actionable icon with no accessible name or state, so assistive-technology users cannot identify what it does. Provide an add/remove-favorite description based on
entry.isFavored.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:911
- The dialog's close button has no accessible name, so screen readers announce an unlabeled control.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:117
- Changing the price-change period while a market fetch is active does not actually fetch the new period:
refreshMarkets()returns early at line 141, leaving the UI set to (for example) 24h while the cached lists and sparklines still contain the in-flight 7d response. Ensure a request for the newly selected period is queued after the active request finishes (or make cancellation propagate and restart it safely).
if (oldSettings.priceChangePeriod != settings.priceChangePeriod) {
refreshMarkets()
}
| _uiState.value = | ||
| _uiState.value.copy( | ||
| isLoading = false, | ||
| hasError = results.allFailed, |
| if (period == MarketPriceChangePeriod.SEVEN_DAYS) { | ||
| return markets | ||
| } |
| if (entry.isFavored) { | ||
| AnalyticsTracker.MarketSource.MORE_FAVORITES | ||
| } else { | ||
| AnalyticsTracker.MarketSource.MORE_MARKET_CAP |
| IconButton(onClick = onScan) { | ||
| Icon( | ||
| painter = painterResource(R.drawable.ic_bot_category_scan), | ||
| contentDescription = null, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (7)
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:134
- The 7-day period is the persisted default, but this branch makes every Perpetual Top Gainers/Top Losers tab return the same unsorted list, while the row renderer also shows
--for every change. Until 7-day perpetual data exists, either hide/disable that period for the Perpetual tab or explicitly fall back to 24-hour values so these tabs remain functional.
if (period == MarketPriceChangePeriod.SEVEN_DAYS) {
return markets
}
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:142
- A period change can be dropped here while the initial request is active. The running request keeps the old
duration,applyDisplaySettings()callsrefreshMarkets(), and this early return prevents a request for the new duration; Crypto gainers/losers then remain ordered for the old period until another external refresh. Cancel/restart the old request or queue one refresh with the latest settings.
fun refreshMarkets() {
if (marketRefreshJob?.isActive == true) return
marketRefreshJob =
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:177
allFailedmasks failures for the category the user is viewing whenever any unrelated request succeeds. For example, iftrendingfails butallsucceeds, the default Crypto/Trending page is empty and reports “No Markets” instead of a network error; stale category data can likewise survive a duration change. Track loading/error state per category (or derive it for the selected tab) rather than using one aggregate flag.
_uiState.value =
_uiState.value.copy(
isLoading = false,
hasError = results.allFailed,
)
app/src/main/java/one/mixin/android/ui/home/web3/MarketFragment.kt:156
- The analytics source is being inferred from the asset's favorite status rather than the list that was clicked. A favored asset shown under Crypto or Stock is therefore reported as
MORE_FAVORITES, whereas the previous market-list path reportedMORE_MARKET_CAP. Pass the selected top tab/list context into this navigation decision so analytics reflects the actual source.
if (entry.isFavored) {
AnalyticsTracker.MarketSource.MORE_FAVORITES
} else {
AnalyticsTracker.MarketSource.MORE_MARKET_CAP
},
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:155
- This clickable search icon has no accessibility label, so TalkBack announces an unlabeled button. Use the existing
Searchstring as its content description.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:162
- This clickable scan icon has no accessibility label, so screen-reader users cannot identify its action. Use the existing
Scanstring as its content description.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:911
- The dialog's close button is unlabeled for screen readers. The existing localized
Closestring can be used directly.
contentDescription = null,
| R.drawable.ic_asset_favorites | ||
| }, | ||
| ), | ||
| contentDescription = null, |
| } | ||
| Spacer(modifier = Modifier.width(4.dp)) | ||
| SortLabel( | ||
| text = "Vol", |
# Conflicts: # app/build.gradle.kts # build.gradle.kts
Keep spot and perpetual favorites independent while sharing the Markets UI.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (5)
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:119
- If a market refresh is already active, this call is ignored, even though that request captured the old duration. Applying 24h while the initial 7d request is running therefore leaves Trending/Gainers/Losers populated from 7d data with no follow-up refresh. Wait for the active job and then refresh using the new period.
if (oldSettings.priceChangePeriod != settings.priceChangePeriod) {
refreshMarkets()
}
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:191
- This icon-only search action has no accessible label, so TalkBack announces an unlabeled button. Use the existing localized Search string as its content description.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:198
- This icon-only scan action has no accessible label, so TalkBack announces an unlabeled button. Use the existing localized Scan string as its content description.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:623
- The favorite
IconButtonhas no content description, leaving its distinct nested action unlabeled to screen-reader users. Provide state-specific “Add to watchlist” / “Remove from watchlist” descriptions.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt:220
- The new favorite action is icon-only and has no content description, so TalkBack cannot identify whether it adds or removes the market. Set a state-specific localized description alongside the image resource.
contentDescription = null,
| val tabs = | ||
| if (topTab == MarketTopTab.WATCHLIST) { | ||
| listOf(MarketSubTab.CRYPTO, MarketSubTab.PERPETUAL) | ||
| } else { |
| is MarketListEntry.Spot -> | ||
| viewModelScope.launch(Dispatchers.IO) { | ||
| val updated = | ||
| tokenRepository.updateMarketFavored( | ||
| entry.market.symbol, | ||
| entry.favoriteId, | ||
| entry.isFavored, | ||
| ) | ||
| if (updated && entry.isFavored && tokenRepository.hasAlertsByCoinId(entry.favoriteId)) { | ||
| _uiState.value = _uiState.value.copy(pendingAlertCoinId = entry.favoriteId) | ||
| } | ||
| } |
| val favoriteMarketIds by viewModel.favoriteMarketIds.collectAsStateWithLifecycle() | ||
| var isUpdatingFavorite by remember(marketId) { mutableStateOf(false) } | ||
| val isFavored = marketId in favoriteMarketIds |
| favoriteIv.setOnClickListener { | ||
| onFavoriteClick(market, isFavored) | ||
| } |
| if (change.signum() >= 0) { | ||
| MixinAppTheme.colors.marketGreen | ||
| } else { | ||
| MixinAppTheme.colors.marketRed |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (4)
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:177
- The search button has no accessibility label, so TalkBack cannot identify its action. Use the existing localized Search string as its content description.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:184
- The scan button has no accessibility label, so screen-reader users cannot distinguish it from the adjacent toolbar actions. Use the existing localized Scan string.
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:485
- This favorite control exposes no label or checked state to accessibility services, so a screen-reader user cannot tell whether activating it will add or remove the market. Provide a state-aware localized content description (for example, “Add to Watchlist” versus “Remove from Watchlist”).
contentDescription = null,
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:217
- These global flags only describe the five spot requests. They clear loading as soon as those requests finish and report an error only when all five fail, even if the selected Stock/Perpetual feed is still loading or its own request failed. On a first load this can show “No Markets” while perpetual data is in flight, or hide a Stock request failure because
allsucceeded. Track loading/error per tab or data source and derive the displayed state from the selected tab.
isLoading = false,
hasError = results.allFailed,
| val uiState: StateFlow<MarketPageUiState> = _uiState.asStateFlow() | ||
|
|
||
| private var favoriteSpotMarkets: List<MarketItem> = emptyList() | ||
| private var favoritePerpetualMarkets: List<PerpsMarket> = emptyList() |
| <androidx.constraintlayout.widget.Guideline | ||
| android:id="@+id/price_sort_guideline" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:layout_marginTop="2dp" | ||
| android:ellipsize="end" | ||
| android:maxLines="1" | ||
| android:textColor="?attr/text_assist" | ||
| android:textSize="14sp" | ||
| app:layout_constraintBottom_toBottomOf="@id/icon_iv" | ||
| app:layout_constraintStart_toStartOf="@id/symbol_tv" | ||
| app:layout_constraintTop_toBottomOf="@id/symbol_tv" | ||
| tools:text="Vol 1.2B" /> | ||
| android:orientation="vertical" | ||
| app:layout_constraintGuide_percent="0.75" /> |
| requireContext() | ||
| .alertDialogBuilder() |
| ) { | ||
| Icon( | ||
| painter = painterResource(R.drawable.ic_close), | ||
| contentDescription = null, |
| R.drawable.ic_title_favorites | ||
| }, | ||
| ), | ||
| contentDescription = null, |
| app:layout_constraintBottom_toBottomOf="parent" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintTop_toTopOf="parent" | ||
| tools:ignore="ContentDescription" /> |
| android:drawableStart="@drawable/selector_market_favorites" | ||
| android:paddingStart="16dp" |
| android:layout_width="24dp" | ||
| android:layout_height="24dp" | ||
| android:background="?android:attr/selectableItemBackgroundBorderless" | ||
| android:padding="3dp" |
| }, | ||
| ), | ||
| ) | ||
| selectedIv.setImageResource( |
Store spot and perpetual ranks, categories, and favorites in their scoped databases. Refresh market page APIs concurrently every 30 seconds while the page is resumed.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 74 out of 75 changed files in this pull request and generated no new comments.
Suppressed comments (4)
app/src/main/res/layout/view_home_toolbar.xml:53
- The settings button is exposed as an unlabeled control to accessibility services. Add the existing localized settings label and remove the lint suppression.
app/src/main/res/layout/view_home_toolbar.xml:41 - The scan button has no accessible label, leaving TalkBack users unable to identify its action. Use the existing localized QR-scan string rather than suppressing the warning.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32 - This job performs only network-backed refreshes, but unlike the refresh jobs it replaces it has no network constraint. While offline it will run all requests immediately, publish a failed refresh, and be queued again every 30 seconds instead of waiting for connectivity. Mark the job as requiring a network connection.
Params(PRIORITY_UI_HIGH)
.singleInstanceBy(GROUP),
app/src/main/res/layout/view_home_toolbar.xml:29
- The search button suppresses the missing-content-description warning, so screen readers announce an unlabeled button. Give this actionable icon a localized description instead of ignoring the lint check.
This issue also appears in the following locations of the same file:
- line 40
- line 52
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 77 out of 78 changed files in this pull request and generated no new comments.
Suppressed comments (6)
app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48
- Ordering category rows by global market-cap rank discards the category endpoint's rank order.
TradeFragmentnow observes this flow and the recommendation UI takes the first eight entries, so trending/top recommendations can show different items than the API selected. Preserve insertion/API order here, as the perpetual category DAO already does.
ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
CAST(mr.market_cap_rank AS INTEGER) ASC
app/src/main/java/one/mixin/android/ui/home/web3/MarketFragment.kt:74
- The refresh loop is tied only to lifecycle events, but these bottom-navigation fragments are switched with
hide()/show(), which does not sendON_PAUSE. After visiting Markets once, it will therefore keep issuing the full market refresh every 30 seconds while another tab is visible. Stop/start refresh fromonHiddenChangedas well (and avoid starting onON_RESUMEwhileisHidden).
DisposableEffect(lifecycleOwner) {
val observer =
LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> viewModel.startRefresh()
Lifecycle.Event.ON_PAUSE -> viewModel.stopRefresh()
else -> Unit
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:210
- These exact, case-sensitive comparisons drop category aliases that the existing perpetual UI supports (
index/indices,commodity/commodities, andfx/forex). Markets carrying the singular orfxvalues disappear from the corresponding merged-page tabs. Keep the aliases and case-insensitive matching.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:348 - The old filter accepted singular/plural category aliases and ignored case, but this replacement only accepts one exact database value. Existing
stock,index,commodity,fx, or differently cased records will no longer appear in their tabs; preserve those aliases when filtering.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListAdapter.kt:79 - This changes the icon optimistically but the callback exposes no failure result to the adapter. If the request fails, the favorites flow does not emit, so the row remains visually toggled (and repeated taps keep using the stale captured
isFavored) until some unrelated rebind. Render fromfavoriteMarketIds, or explicitly roll back on failure.
app/src/main/res/layout/item_market_list.xml:16 - The newly clickable favorite control has only a 24×24 dp hit target, half the Android-recommended 48×48 dp minimum. This makes favoriting difficult for users with limited dexterity; enlarge its hit area while keeping the visible star at its current size.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 79 changed files in this pull request and generated no new comments.
Suppressed comments (6)
app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1379
- A successful response with a null
datapayload is converted to an empty list here. Forallandfavorite, the transaction then clears the rank/favorite tables and the refresh job reports success, so a malformed response can erase valid cached market state. Preserve null as a failed refresh; an actual empty list can still clear the cache intentionally.
val markets = response.data.orEmpty()
app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt:161
- Converting a successful null payload to
emptyList()makessyncFavoriteMarketsandsyncCategoryreplace their cached relations with nothing while reporting a successful refresh. Return null for a missing payload so malformed responses retain the last valid cache; a genuine empty list remains distinguishable.
successBlock = { response ->
response.data.orEmpty().map(PerpsMarket::withDefaults)
app/src/main/res/layout/view_home_toolbar.xml:41
- The scan action has no accessible label, so assistive technology cannot identify its purpose. Use the existing localized Scan string rather than suppressing the content-description warning.
app/src/main/res/layout/view_home_toolbar.xml:53 - The settings action is exposed as an unlabeled image button to screen readers. Add the existing localized Settings description instead of ignoring the lint check.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32 - This combined refresh job performs only network requests but no longer carries
requireNetwork(), unlike both jobs it replaces. When queued offline it runs immediately, marks every source failed, and exits instead of waiting for connectivity; restoring the constraint avoids false error states and unnecessary request bursts.
Params(PRIORITY_UI_HIGH)
.singleInstanceBy(GROUP),
app/src/main/res/layout/view_home_toolbar.xml:29
- The search action has no accessible label; suppressing the lint warning leaves screen-reader users with an unlabeled button. Provide the existing localized Search string as its content description.
This issue also appears in the following locations of the same file:
- line 40
- line 52
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 79 changed files in this pull request and generated no new comments.
Suppressed comments (8)
app/src/main/res/layout/view_home_toolbar.xml:41
- This icon-only scan action has no accessible name, so screen-reader users cannot identify it. Use the existing localized Scan string instead of suppressing the warning.
app/src/main/res/layout/view_home_toolbar.xml:53 - The settings icon is also unlabeled for accessibility services. Replace the suppression with the existing localized Settings description.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:156 - This exact comparison drops category aliases that are still recognized elsewhere (
PerpetualContent.kt:654-659) and were recognized by the replaced implementation. For example, a market with categorystockorcommodityno longer appears under Stocks or Commodities. Preserve the aliases and case-insensitive matching for every category.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32 - This network-only job lacks
requireNetwork(), unlike the jobs it replaces and other refresh jobs. While offline it will run and fan out all refresh requests immediately, then the page loop schedules the same failing work again every 30 seconds. Defer execution until connectivity is available;singleInstanceBywill still coalesce queued refreshes.
Params(PRIORITY_UI_HIGH)
.singleInstanceBy(GROUP),
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:210
- These exact, case-sensitive comparisons regress the aliases the existing perps UI still handles (
PerpetualContent.kt:654-659) and that the removed bottom sheet accepted (index/indices,commodity/commodities, andforex/fx). Markets using a singular alias or different case will disappear from the corresponding merged-page tab. Match the supported aliases case-insensitively.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:167 - Favorites and featured markets are now fetched on every 3-second price refresh, tripling this sheet's polling traffic even though local favorite changes already update the database and featured data is not tick data. Refresh these two once when collection resumes (or on a substantially slower cadence), while keeping only market prices in the tight loop.
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:130 - The merged page does not track a favorite update as pending or apply an optimistic state. Until the repository flow emits, repeated taps reuse the same stale
entry.isFavoredvalue and launch duplicate identical requests; a quick add-then-remove gesture therefore remains added, and concurrent responses can overwrite user intent. Add a per-entry pending/override state (as the new perps bottom sheet does) or disable the action until completion.
app/src/main/res/layout/view_home_toolbar.xml:29 - Suppressing the warning leaves the search action unlabeled for TalkBack and other accessibility services. Give this icon-only button its existing localized Search label.
This issue also appears in the following locations of the same file:
- line 41
- line 53
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 79 changed files in this pull request and generated no new comments.
Suppressed comments (6)
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:155
- The Watchlist filter ignores
favoriteOverrides, even though the row icon is rendered fromisFavorite. During a pending removal the row remains in Watchlist with an unselected star, and during a pending addition it remains absent. Use the effective state here so optimistic updates are applied consistently.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:33 - The new refresh job performs only network requests but is not constrained to network availability. Offline runs therefore execute immediately, publish every source as failed, and are discarded instead of waiting for connectivity; add the same
requireNetwork()constraint used by the other refresh jobs.
Params(PRIORITY_UI_HIGH)
.singleInstanceBy(GROUP),
app/src/main/java/one/mixin/android/db/MixinDatabaseMigrations.kt:618
- This migration adds a new Room table but has no 71→72 migration test. The repository has dedicated migration coverage (including the new perps 6→7 test in this PR), so please add a
runMigrationsAndValidatetest that starts at version 71 and verifies existing data plus the new table/index.
val MIGRATION_71_72: Migration =
object : Migration(71, 72) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `market_categories` (`coin_id` TEXT NOT NULL, `category` INTEGER NOT NULL, PRIMARY KEY(`coin_id`, `category`))")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_market_categories_category` ON `market_categories` (`category`)")
app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt:1429
- The previous market refresh path explicitly handled
OLD_VERSIONby showing the mandatory update dialog.refreshMarketsByCategorynow delegates toTokenRepository.fetchMarkets, which silently converts all API failures tonull, so an old-client response from these endpoints no longer prompts the user. Preserve the error code through the repository/view-model boundary and retain the update handling.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:53 favoriteOverridesis the effective favorite state while a request is pending, but recommendation filtering uses only the persisted IDs. After an optimistic add/remove, a market can therefore simultaneously appear with its new star state and in the recommendation set. Filter throughisFavoriteso the list is consistent with the rendered state.
This issue also appears on line 152 of the same file.
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketFavoriteIcon.kt:91
- A trigger reset is currently treated as a new animation because any unequal value plays. Callers reset the counter when the market identity changes (
remember(marketId)), while this helper is remembered independently, so navigating to another market after an animation can spuriously replay it. Since all callers increment the counter for events, only a larger value should play.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 78 out of 79 changed files in this pull request and generated no new comments.
Suppressed comments (5)
app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:62
- This second category lookup has the same ranking regression: market-cap order replaces the category endpoint's own order. Keep it consistent with
observeMarketsByCategoryand return rows in relation insertion order.
ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
CAST(mr.market_cap_rank AS INTEGER) ASC
app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48
- These category rows are inserted in the API's ranked order, but sorting them by the separate all-market cap rank changes the
trending,top_gainers, andtop_losersorder before the trade page takes its first eight recommendations. Preserve the relation insertion order here, as the perpetual category DAO does, so the category endpoint's ranking survives persistence.
ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
CAST(mr.market_cap_rank AS INTEGER) ASC
app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1379
- A successful response envelope with a null
datapayload is treated as an authoritative empty result. The following transaction then clears market-cap ranks, favorites, or category relations, making cached market lists disappear on a malformed response. Return a failed sync for null while still allowing a real empty list to clear a category.
val markets = response.data.orEmpty()
app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt:162
- Converting a null successful payload to
emptyList()makessyncFavoriteMarketsandsyncCategorydelete their cached relations and report success. Preserve the cache by propagating null as a failed sync; a non-null empty list can still intentionally clear it.
successBlock = { response ->
response.data.orEmpty().map(PerpsMarket::withDefaults)
},
app/src/main/res/values-zh-rTW/strings.xml:922
- The new removal flow uses
watchlist_remove_desc, but this locale does not define it, so Traditional Chinese users see the English fallback after removing a favorite. Add the localized toast alongside the newly added watchlist strings.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 83 out of 84 changed files in this pull request and generated no new comments.
Suppressed comments (2)
app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48
- This discards the API's category rank and reorders every category by market-cap rank.
SwapRecommendedMarketCardsthen takes only the first eight entries (SwapRecommendedMarketCards.kt:78-103), so Trending and Top Gainers/Losers can show the largest-cap assets instead of the API's top-ranked assets. Preserve the insertion/API order, as the perpetual category DAO does.
ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
CAST(mr.market_cap_rank AS INTEGER) ASC
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:166
- The resumed loop now performs three network refreshes every three seconds. Favorites and featured recommendations do not need market-ticker frequency, so keeping them inside this loop triples request volume while the sheet is open; refresh those two once per resume and poll only market prices.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 83 out of 84 changed files in this pull request and generated no new comments.
Suppressed comments (7)
app/src/main/res/layout/view_home_toolbar.xml:53
- This settings action has no accessible name, making the reusable toolbar's third action indistinguishable to screen readers. Use the existing Settings string as its content description.
app/src/main/res/layout/view_home_toolbar.xml:41 - This scan action is unlabeled for screen-reader users. Add the existing Scan string as its content description rather than suppressing the warning.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:206 - The recommendation request can complete after the dialog is dismissed, at which point Fragment.getString throws on the detached fragment. Use the application context to format this asynchronous toast.
app/src/main/res/layout/view_home_toolbar.xml:29 - This search button has no accessibility label, so screen readers announce an unlabeled control. Provide the existing Search string as its content description instead of suppressing the lint warning.
This issue also appears in the following locations of the same file:
- line 40
- line 52
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:167
- The 3-second loop now refreshes all markets, favorites, and featured markets every iteration. The latter two calls duplicate market upserts and turn one polling request into three, increasing battery/data use and backend load while the sheet is open. Refresh the relatively static favorite/featured membership once, and keep only price data in the polling loop.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:182 - This callback is owned by the ViewModel and can finish after the bottom sheet has been dismissed. Calling Fragment.getString then throws because the fragment is detached; format the toast with the application context, as the other market flow does.
This issue also appears on line 206 of the same file.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
- This job performs only network requests but lacks the network constraint used by the other refresh jobs (for example, RefreshSnapshotsJob.kt:10 and RefreshPerpsPositionsJob.kt:13). Without it, the 30-second UI loop runs all refresh attempts while offline and emits avoidable failure states instead of letting the queue defer work until connectivity returns.
Params(PRIORITY_UI_HIGH)
.singleInstanceBy(GROUP),
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 87 out of 88 changed files in this pull request and generated no new comments.
Suppressed comments (6)
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:179
- Rolling back only the optimistic override does not roll back the favorite icon.
MarketListRowFramestill retains the attempted animation intent, and the animation state snaps to that intent’s target until it receivesnull; therefore a failed add/remove continues to display the wrong favorite state. Clear the row’s intent on failure or make the favorite state carry a reset signal to the row.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:156 - The new category filter drops aliases that this screen already receives.
PerpetualContent.kt:654-659treatsstock/stocksandcommodity/commoditiesas equivalent, and the removed implementation also acceptedindex,fx, and case variants. With exact equality here, tapping “View all” can produce an empty list for markets shown in the preview. Preserve those aliases (and case-insensitive matching) in this filter.
app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:167 - This loop now performs three network refreshes every three seconds. Favorites and featured recommendations are not quote streams, so polling both alongside market prices triples request volume while the sheet is open and wastes battery/data. Refresh those two once on resume, then keep only
refreshMarkets()in the interval loop.
This issue also appears on line 178 of the same file.
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:131
- The favorite requests discard their success result. The row starts a local
MarketFavoriteAnimationIntentbefore invoking this method, and that intent must be cleared to return to the authoritative value. If a request fails, the database state stays unchanged but the icon remains latched in the attempted state. Propagate completion back to the row and clear its animation intent on failure, as the detail page does.
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:217 - These exact category comparisons repeat the alias regression in the main market page. Existing perps data handling accepts singular values such as
indexandcommodity(andfxfor forex), so those markets disappear from the corresponding merged tabs. Match the existing aliases case-insensitively rather than requiring only the plural database values.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32 - This is a network-only refresh job, but unlike the two jobs it replaces and the other refresh jobs, it no longer declares
requireNetwork(). Offline runs therefore execute every request immediately, mark every source failed, and repeat the work every refresh interval instead of waiting for connectivity. Restore the network constraint.
Params(PRIORITY_UI_HIGH)
.singleInstanceBy(GROUP),
Keep the animation intent until Lottie playback completes when the favorite request returns first.
Restore full list rendering while keeping the remaining market list performance optimizations.
No description provided.