Skip to content

Feat/merge market pages - #6517

Open
SeniorZhai wants to merge 34 commits into
masterfrom
feat/merge-market-pages
Open

Feat/merge market pages#6517
SeniorZhai wants to merge 34 commits into
masterfrom
feat/merge-market-pages

Conversation

@SeniorZhai

Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings July 23, 2026 07:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +115 to +117
if (oldSettings.priceChangePeriod != settings.priceChangePeriod) {
refreshMarkets()
}
Comment on lines +108 to +111
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,
Copilot AI review requested due to automatic review settings July 23, 2026 07:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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()
            }

Comment on lines +173 to +176
_uiState.value =
_uiState.value.copy(
isLoading = false,
hasError = results.allFailed,
Comment on lines +132 to +134
if (period == MarketPriceChangePeriod.SEVEN_DAYS) {
return markets
}
Comment on lines +152 to +155
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,
Copilot AI review requested due to automatic review settings July 23, 2026 07:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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() calls refreshMarkets(), 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

  • allFailed masks failures for the category the user is viewing whenever any unrelated request succeeds. For example, if trending fails but all succeeds, 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 reported MORE_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 Search string 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 Scan string 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 Close string 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.
Copilot AI review requested due to automatic review settings July 24, 2026 03:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 IconButton has 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,

Comment on lines +241 to +244
val tabs =
if (topTab == MarketTopTab.WATCHLIST) {
listOf(MarketSubTab.CRYPTO, MarketSubTab.PERPETUAL)
} else {
Comment on lines +124 to +135
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)
}
}
Comment on lines +113 to +115
val favoriteMarketIds by viewModel.favoriteMarketIds.collectAsStateWithLifecycle()
var isUpdatingFavorite by remember(marketId) { mutableStateOf(false) }
val isFavored = marketId in favoriteMarketIds
Comment on lines +60 to +62
favoriteIv.setOnClickListener {
onFavoriteClick(market, isFavored)
}
Comment on lines +539 to +542
if (change.signum() >= 0) {
MixinAppTheme.colors.marketGreen
} else {
MixinAppTheme.colors.marketRed
Copilot AI review requested due to automatic review settings July 27, 2026 14:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 all succeeded. 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()
Comment on lines +65 to +70
<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" />
Comment on lines +129 to +130
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" />
Comment on lines +101 to +102
android:drawableStart="@drawable/selector_market_favorites"
android:paddingStart="16dp"
Comment on lines +13 to +16
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.
Copilot AI review requested due to automatic review settings July 28, 2026 10:46
Copilot AI review requested due to automatic review settings July 30, 2026 10:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48

  • This reorders every category by market cap, so the Trending and Featured lists no longer preserve the order returned by their category endpoints. replaceCategory inserts relations in response order; order this query by that relation order (or persist an explicit category rank) instead.
        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/res/layout/view_home_toolbar.xml:41

  • The scan button is actionable but has no accessibility label, so screen readers cannot identify it. Replace the lint suppression with the existing Scan string as its content description.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings button is actionable but has no accessibility label, so screen readers cannot identify it. Replace the lint suppression with the existing Settings string as its content description.
    app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:34
  • MarketPageViewModel uses observeAllMarkets() as the Perpetual Trending source, and the mapper intentionally preserves its input order. Sorting here by volume therefore turns Trending into a volume ranking and defeats the API-rank behavior covered by perpetualTrendingPreservesApiRankOrder. Preserve insertion/API order instead.
        ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC

app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:150

  • Global refresh failures set state.hasError, but the indicator branch does not pass that state to IndicatorPage. With no cached indicator, a failed GLOBAL request therefore displays “No Markets” instead of the network error used by the other tabs. Pass hasError through and select Network_error for this case.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • The search button is actionable but has no accessibility label, so screen readers cannot identify it. Replace the lint suppression with the existing Search string as its content description.

This issue also appears in the following locations of the same file:

  • line 40
  • line 52

Copilot AI review requested due to automatic review settings July 30, 2026 12:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (9)

app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48

  • Category responses were previously consumed in API order, but this DAO now reorders every category by global market-cap rank. Since Trending is not sorted again by MarketPageMapper and Trade also observes this flow, both surfaces lose the API's category ranking. Store each category response position on the relation and order by that rank.
        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/res/layout/view_home_toolbar.xml:41

  • This standalone scan button is exposed without an accessible name. Replace the lint suppression with the existing Scan label so screen readers can identify the action.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings button is unlabeled for accessibility services. Use the existing Settings string instead of suppressing the content-description warning.
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:186
  • The category tabs now require exact plural, lowercase values. Cached/API markets can still use aliases such as stock/stocks and commodity/commodities (see PerpetualContent.kt:654-659), so those markets disappear from these tabs. Preserve the previous alias- and case-insensitive matching.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:345
  • This exact comparison regresses the alias and case-insensitive matching previously used by this sheet. Markets categorized as stock, index, commodity, or fx will no longer appear under their corresponding filters.
    app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:34
  • MarketPageViewModel feeds observeAllMarkets() directly to the Perpetual Trending tab, whose mapper intentionally preserves source order. Ordering this query by volume therefore replaces the API trending rank with a volume rank, contrary to perpetualTrendingPreservesApiRankOrder. Persist and query an API/category rank instead of imposing volume order here; the identical getAllMarkets query needs the same treatment.
        ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC

app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:380

  • When a periodic featured/favorite refresh removes a selected recommendation, its ID remains in selectedRecommendationIds. The button can then stay enabled with no visible selection and repeatedly submit an already-favored or no-longer-featured market. Intersect selection with the displayed recommendations whenever the list updates.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:280
  • Favorites and featured membership are not price-tick data, but these two added requests now run every three seconds while the sheet is resumed—up to 40 extra requests per minute. Refresh them once on resume (local mutations already update their database flows) and keep only the all-markets price refresh in the polling loop.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • This standalone search button has no accessible name, and suppressing ContentDescription leaves TalkBack users with an unlabeled control. Use the existing Search string as its content description.

This issue also appears in the following locations of the same file:

  • line 41
  • line 53

Clearing the ImageView padding let Lottie fill the 40dp touch target, while the static icon and perpetual detail implementation used smaller visual bounds. Keep the animation at 24dp and the final icon at 22dp.
Copilot AI review requested due to automatic review settings July 30, 2026 12:29
@SeniorZhai
SeniorZhai force-pushed the feat/merge-market-pages branch from f042596 to 8795572 Compare July 30, 2026 12:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 65 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (10)

app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48

  • All category results are reordered by global market-cap rank, while market_categories stores no position from the category response. This loses the API ranking/curation for Trending and Featured; the previous Trade flow consumed those response lists in order. Store a per-category position and order ranked categories by it.
        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/res/layout/view_home_toolbar.xml:41

  • This scan action is also exposed as an unlabeled ImageButton. Add the existing Scan string as its content description so assistive technology can identify it.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The shared third button has no accessible name, and its action differs by caller (Settings in Explore versus Market Display in MarketPage). Expose a content-description setter/attribute and set the caller-specific label; a static Settings label would be incorrect on the market screen.
    app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:24
  • The new Trending path receives observeAllMarkets() and leaves its order unchanged, but this query now orders every result by volume. Consequently the actual UI cannot preserve API rank order as the new perpetualTrendingPreservesApiRankOrder test expects. Persist the API position (or provide a dedicated ranked query) instead of replacing it with volume order.
        WHERE CAST(volume AS REAL) > 0
        ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC

app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:186

  • These exact, case-sensitive comparisons drop category aliases still supported elsewhere in the codebase (PerpetualContent.kt:654-659 accepts stock/stocks and commodity/commodities; the previous list also accepted index/indices and fx/forex). Markets using a singular alias or different case will disappear from the merged category tabs. Normalize on ingestion or retain the aliases here.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:345
  • The bottom-sheet category filter now uses an exact, case-sensitive database value, regressing the aliases previously accepted here (stock/stocks, index/indices, commodity/commodities, and fx/forex). Those values are also still recognized by PerpetualContent.kt:654-659. Normalize categories or match the supported alias sets so valid markets remain visible.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:280
  • Favorites and Featured are account metadata/curated lists, but this loop now re-fetches both every three seconds in addition to prices—40 unnecessary requests per minute while the sheet is resumed. Local favorite mutations already update Room. Refresh these two sources once on resume (or after mutation) and keep only market prices in the ticker.
    app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt:59
  • This snapshot replacement can race updateFavorite/addFavoriteMarkets: a GET started before the user's POST can finish afterward and overwrite the newer local favorite state with its stale snapshot. The three-second refresh loop makes this likely. Serialize favorite syncs and mutations with a repository-level mutex (including the network call and DAO write).
        suspend fun syncFavoriteMarkets(): List<PerpsMarket>? {
            val markets = fetchMarkets(CATEGORY_FAVORITE) ?: return null
            database.withTransaction {
                marketDao.upsertList(markets)
                favoriteDao.replaceAll(
                    marketIds = markets.map(PerpsMarket::marketId),
                    createdAt = nowInUtc(),
                )

app/src/main/res/layout/view_home_toolbar.xml:29

  • This interactive search button has no accessible name; suppressing the lint warning leaves TalkBack announcing an unlabeled control. Use the existing Search string as its content description.

This issue also appears in the following locations of the same file:

  • line 40
  • line 52
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This job consists entirely of network requests but is the only comparable single-instance refresh job that does not call requireNetwork(). Offline runs therefore execute immediately, publish failures, and are discarded instead of waiting for connectivity. Add the network constraint so JobQueue can defer the refresh until it can succeed.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

Comment on lines +163 to +165
successBlock = { response ->
response.data.orEmpty().map(PerpsMarket::withDefaults)
},
)
},
successBlock = { response ->
val markets = response.data.orEmpty()
Copilot AI review requested due to automatic review settings July 31, 2026 02:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 64 out of 65 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (10)

app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48

  • The category response order is discarded here and replaced with global market-cap order. The Trade screen consumes trendingMarkets.take(8), so a server-ranked trending response will now show the largest-cap assets instead of its first eight results. Persist an ordinal with each category relation and order by that ordinal (with deterministic tie-breakers).
        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/res/layout/view_home_toolbar.xml:41

  • This scan action is exposed as an unlabeled button to accessibility services. Add the existing Scan string as its content description.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings action has no accessible label, so screen-reader users cannot identify it. Use the existing Settings string instead of suppressing ContentDescription.
    app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:34
  • This volume sort means the Perpetual “Trending” tab cannot preserve API rank order: MarketPageViewModel reads this flow and the mapper intentionally leaves trending entries in source order. Store and query an API ordinal (or otherwise retain the response order) instead of substituting volume order.
        ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC

app/src/main/res/layout/view_home_toolbar.xml:29

  • This actionable search button has no content description, so TalkBack announces an unlabeled button. Use the existing Search string instead of suppressing the lint warning.

This issue also appears in the following locations of the same file:

  • line 41
  • line 53
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:908
  • This hard-coded user-visible label bypasses the perps_badge resource added in this PR. Reading it from the resource keeps the badge definition centralized and avoids diverging from resource-based UI text.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:380
  • When favorites change, the recommendation list can shrink while selectedRecommendationIds still contains IDs for cards that are no longer visible. If those IDs are already favorites, addFavoriteMarkets returns an empty set, so the callback removes nothing and leaves the Add button enabled with no visible selection. Intersect the selection with the current recommendation IDs whenever this list is rebuilt.
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:126
  • Favorite mutations are not guarded per entry. Until the database flow emits, rapid taps reuse the same stale entry.isFavored value and launch duplicate add/remove requests rather than toggling back. Track in-flight favorite IDs (or optimistically update with rollback) and disable that entry’s action until the request completes, as the detail page already does.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListAdapter.kt:70
  • This click handler captures isFavored from the last bind and remains enabled while the network request is pending. A quick second tap therefore submits the same operation again instead of reversing it. Disable or debounce this market’s favorite action until the favorite-ID flow reflects completion.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:278
  • These two calls run on every 3-second quote refresh, adding about 40 favorites/featured requests per minute while the sheet is open. Those datasets do not need quote-level polling and local favorite mutations already update the database. Refresh them once on resume (or on a much slower cadence) and keep only refreshMarkets() in this loop.

Copilot AI review requested due to automatic review settings July 31, 2026 07:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 67 out of 68 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

  • The category API's ranking is discarded here and replaced with global market-cap order. Both the merged market page's Trending branch and SwapRecommendedMarketCards consume this flow without re-sorting (the latter takes the first eight), while the previous direct API path preserved response order. Persist the category response position in MarketCategoryRelation and order by it so Trending/Featured recommendations retain their server-defined ranking.
        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/res/layout/view_home_toolbar.xml:41

  • The scan button has no accessibility label, so assistive technologies cannot identify its action. Use the existing Scan string resource instead of suppressing the warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings button is unlabeled for screen readers. Replace this suppression with the existing Settings string resource.
    app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:34
  • This volume sort prevents the Perpetual Trending list from preserving API rank: MarketPageMapper.perpetualMarkets(..., TRENDING) deliberately keeps its input order (and the new test asserts that behavior), but the UI receives this DAO flow already reordered by volume. Store and query an API position/rank (or otherwise preserve response order) instead of imposing volume order for the all-markets flow.
        ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC

app/src/main/res/layout/view_home_toolbar.xml:29

  • The search button has no accessibility label, so screen readers announce an unlabeled control. Replace the lint suppression with the existing Search string resource.

This issue also appears in the following locations of the same file:

  • line 41
  • line 53
    app/src/main/java/one/mixin/android/ui/wallet/MarketDetailsFragment.kt:129
  • The favorite control remains enabled while this asynchronous request runs, and line 141 immediately flips marketItem.isFavored. A quick second tap can therefore enqueue the inverse operation; if the first removal completes later, its callback can offer to delete alerts even though the market has already been re-added. Disable/serialize the control until the request completes and only show the destructive prompt when the latest state is still removed.

Copilot AI review requested due to automatic review settings July 31, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 69 out of 70 changed files in this pull request and generated no new comments.

Suppressed comments (4)

app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48

  • This ordering discards the order returned by ranked category endpoints. replaceCategory inserts relations in response order, but trending/featured consumers now receive market-cap order; the previous TradeFragment path used the API list directly, and the equivalent perps DAO deliberately orders by relation rowid. Order by mc.rowid so API-ranked categories remain ranked as returned.
        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/job/RefreshMarketPageJob.kt:32

  • This replacement refresh job performs only network work, but unlike the removed refresh jobs it has no network constraint. When launched offline, JobQueue executes all requests immediately, records every source as failed, and removes the job instead of waiting for connectivity. Preserve the previous retry-on-connect behavior by requiring a network here (as other refresh jobs such as RefreshSnapshotsJob do).
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:199

  • These exact plural comparisons regress the category aliases supported by the existing perps UI. Migration 6→7 preserves cached markets, so cached values such as index, commodity, or fx (and differently cased values) disappear from the merged page until replaced. Keep the prior singular/plural, case-insensitive matching when filtering.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:345
  • The bottom sheet previously accepted singular/plural aliases and ignored case, but this exact comparison now hides preserved cached categories such as stock, index, commodity, and fx. PerpetualContent.kt:654-659 still treats these aliases as equivalent. Normalize category values or restore alias sets before filtering.

Copilot AI review requested due to automatic review settings July 31, 2026 09:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 68 out of 69 changed files in this pull request and generated no new comments.

Suppressed comments (5)

app/src/main/java/one/mixin/android/db/MarketCategoryDao.kt:48

  • This orders every ranked category by the global market-cap rank rather than by the category API's rank. TradeFragment now consumes this flow and the recommendation cards call take(8), so Trending/Top Gainers/Top Losers can show the largest-cap assets instead of the category leaders. Preserve the relation insertion/API order, as PerpsMarketCategoryDao 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/res/layout/view_home_toolbar.xml:41

  • The scan action has no accessible name, so screen-reader users only encounter an unlabeled button. Use the existing localized Scan QR string instead of suppressing the lint warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings/display action has no accessible name, so screen-reader users only encounter an unlabeled button. Use the existing localized Settings string instead of suppressing the lint warning.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • The search action has no accessible name, so screen-reader users only encounter an unlabeled button. Use the existing localized Search string 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:281
  • These favorite and featured syncs are inside the 3-second price polling loop, adding up to 40 extra network requests per minute while this sheet is open. Refresh these membership datasets once when the lifecycle collection starts (or on a substantially slower cadence), and keep only market prices in the fast loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants