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", |
No description provided.