Skip to content

Feat/merge market pages - #6517

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

Feat/merge market pages#6517
SeniorZhai wants to merge 29 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 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 (4)

app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt:164

  • orEmpty() makes a successful response with null data indistinguishable from an authoritative empty market list. syncFavoriteMarkets and syncCategory will then delete all local favorites/category rows, so a malformed response can empty the watchlist; propagate null instead while still accepting a real empty list.
                    response.data.orEmpty().map(PerpsMarket::withDefaults)

app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32

  • This job performs only network requests but has no network constraint. When opened offline it runs immediately, treats every source as failed, and is considered finished rather than waiting for connectivity; unlike the jobs it replaces, the refresh can therefore be lost when the page is paused before the next interval. Add the queue's network requirement.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1379

  • A successful response with a missing data payload is converted to an empty list, so the transaction below clears market-cap ranks, favorites, or category membership as if the server had authoritatively returned no markets. Preserve the cache on malformed/null payloads by returning null; an actual empty list can still clear the corresponding relation.
                val markets = response.data.orEmpty()

app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1423

  • All route failures are suppressed here. The replaced TradeFragment.fetchRecommendedMarket path explicitly handled ErrorHandler.OLD_VERSION with the required update dialog; after this change an outdated client silently keeps empty/stale recommendations instead. Propagate that error or restore the old-version handling at the caller.
            failureBlock = { true },
            exceptionBlock = { true },
            defaultErrorHandle = {},
            defaultExceptionHandle = {},

Copilot AI review requested due to automatic review settings July 30, 2026 08:32

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

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

  • Category membership stores no response ordinal, and this query reorders every category by market-cap rank. Consequently API-ranked Trending and Featured results lose their order before MarketPageViewModel consumes them unchanged. Persist each item's response position in the relation and order by that position 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 action is exposed as an unlabeled image button to accessibility services. Provide the existing localized Scan description rather than suppressing the warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings/display action is unlabeled for screen readers. Add the existing localized Settings description instead of suppressing the warning.
    app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1379
  • A successful response with a null data payload is converted to an empty list. For the all and favorite requests, that immediately runs replaceAll(emptyList()), clearing cached ranks or favorites while reporting a successful refresh. Treat null as a failed refresh; an explicit empty array can still clear the category.
                val markets = response.data.orEmpty()

app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt:164

  • Null response data is treated as a valid empty market list. syncFavoriteMarkets and syncCategory then replace their relation tables with that empty list, so a malformed successful response can erase the cached watchlist/recommendations. Preserve null so these sync methods abort without modifying local data.
                    response.data.orEmpty().map(PerpsMarket::withDefaults)

app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:24

  • These queries now force volume order, but the Trending path consumes observeAllMarkets() unchanged and the new test explicitly expects API rank order. This guarantees Trending is displayed by volume instead. Preserve the API/insertion order here, or persist a separate API rank if other screens need volume order.
        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:455

  • Before the selected Room flows emit, hasLoadedLocalData is false and showsMarketLoading is intentionally false, so this empty branch flashes “No Markets”/“watchlist empty” while local data is still pending. Handle the not-yet-loaded state before rendering an empty result.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This job performs eleven network refreshes but has no network constraint. While offline, the 30-second loop repeatedly executes and fails all requests instead of retaining one pending single-instance refresh, wasting work and surfacing avoidable failures. Add the same network requirement used by the other refresh jobs.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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

  • This also triggers the add animation optimistically; both spot and perpetual updates can fail while the row still plays a success animation. Have onFavorite report completion and increment the trigger only after a successful add.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt:224
  • The favorite animation starts before the repository result is known, so a failed add still presents a success animation. Trigger it only inside the successful completion path.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • The shared toolbar's search action has no accessible name, so screen-reader users encounter an unlabeled button on both Market and Explore. Use the existing localized label instead of suppressing the lint warning.

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

  • line 41
  • line 53

Copilot AI review requested due to automatic review settings July 30, 2026 09:18

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 1 comment.

Comments suppressed due to low confidence (6)

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

  • Category results are persisted in API order by replaceCategory, but this query reorders every category by global market-cap rank. Consequently Trending (and the Trade page's first eight recommendations) no longer reflects the ranking returned by the category API. Preserve the relation insertion order here; gainers/losers are already explicitly sorted by the selected period in MarketPageMapper.
        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 shared scan button has no accessible name, so screen-reader users cannot identify its action. Use the existing Scan string instead of suppressing the lint warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The shared settings button is exposed without an accessible name. Add the existing Settings label rather than suppressing ContentDescription.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This job consists entirely of network requests but has no network constraint. When queued offline it runs immediately, marks every source failed, and is discarded instead of waiting for connectivity; comparable refresh jobs use requireNetwork(). Add the constraint so cached data remains usable and refresh resumes when the network returns.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:34

  • The merged page feeds observeAllMarkets() into the Trending tab, while MarketPageMapper.perpetualMarkets(..., TRENDING) deliberately preserves its input order. Sorting this DAO flow by volume therefore defeats the API ranking and contradicts the new perpetualTrendingPreservesApiRankOrder behavior. Persist/query an explicit API rank (or a Trending category relation) rather than using volume order for this source.
        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 shared search button has no accessible name, and suppressing ContentDescription makes TalkBack announce an unlabeled control on both Explore and Markets. Provide the existing Search string as its content description.

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

  • line 40
  • line 52

@Composable
private fun BitcoinDominanceCard(indicator: GlobalMarket) {
val dominance =
indicator.dominancePercentage
Copilot AI review requested due to automatic review settings July 30, 2026 10:11

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

app/src/main/java/one/mixin/android/ui/home/web3/MarketFragment.kt:72

  • The refresh loop is tied to ON_RESUME/ON_PAUSE, but home-tab navigation hides and shows fragments without changing their lifecycle (NavigationController.navigate uses hide/show). Once this fragment has resumed, switching to another tab will not call stopRefresh(), so the hidden market page continues issuing the full refresh batch every 30 seconds. Start/stop the loop from fragment visibility (for example onHiddenChanged) in addition to lifecycle state.
                                    Lifecycle.Event.ON_RESUME -> viewModel.startRefresh()
                                    Lifecycle.Event.ON_PAUSE -> viewModel.stopRefresh()

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

  • The scan action has no accessible label and the lint warning is suppressed, leaving screen-reader users unable to identify it. Add the existing Scan string as its content description.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings action is also exposed as an unlabeled button because the content-description warning is suppressed. Label it with the existing Settings string so the shared toolbar is navigable with TalkBack.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This job performs only network requests but no longer declares requireNetwork(). The job queue will run it while offline, mark every source failed, discard the work, and show an avoidable error state instead of waiting for connectivity; the surrounding refresh jobs consistently use requireNetwork(). Restore the network constraint on the consolidated job.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1379

  • Treating a successful response with a missing body as an empty list makes the following transaction clear the cached favorites, ranks, or category relations and still report the refresh as successful. Previously the null body failed before mutating the cache. Return null for a missing body so callers preserve local data and mark this source failed.
                val markets = response.data.orEmpty()

app/src/main/java/one/mixin/android/repository/PerpsMarketRepository.kt:164

  • A null response body is converted to an empty market list, so syncFavoriteMarkets/syncCategory can delete all local favorite or category rows and report success when the server returned no data. Propagate null instead; an actual empty list can still intentionally clear the relation.
                    response.data.orEmpty().map(PerpsMarket::withDefaults)

app/src/main/java/one/mixin/android/repository/TokenRepository.kt:1423

  • All route failures are swallowed here, including OLD_VERSION. The removed TradeFragment implementation explicitly showed the mandatory update dialog for that response; after this refactor an obsolete client silently keeps empty or stale recommendation sections. Preserve an OLD_VERSION signal/callback so the existing update flow can still be shown.
            failureBlock = { true },
            exceptionBlock = { true },
            defaultErrorHandle = {},
            defaultExceptionHandle = {},

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

  • The reusable search action explicitly suppresses the missing content description, so TalkBack announces an unlabeled button on both Explore and Markets. Use the existing 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/market/MarketPage.kt:739
  • The favorite control's clickable bounds are only 24dp square, half of Android's 48dp minimum touch target. This makes the primary watchlist action difficult for users with motor impairments; enlarge the interactive container while keeping the icon visually 16dp.
    app/src/main/res/layout/item_market_list.xml:15
  • This newly clickable favorite icon has a 28×32dp touch target, below Android's 48×48dp accessibility minimum. Increase the interactive view bounds (the icon itself can remain 16dp) so favoriting is reliably usable.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:278
  • The 3-second price polling loop now also re-downloads and rewrites favorites and featured-category membership on every iteration. Those relations do not need tick-level updates, and the all-market refresh already updates prices, so this triples request/transaction load while the sheet is open. Refresh favorites/featured once on entry or on a substantially slower cadence.

Use persisted category values and keep market header labels compact.
Copilot AI review requested due to automatic review settings July 30, 2026 10:18
@SeniorZhai
SeniorZhai force-pushed the feat/merge-market-pages branch from 8dd3574 to 2acda69 Compare July 30, 2026 10:18

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

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

  • This scan action is unlabeled for screen-reader users. Add the existing localized Scan string rather than suppressing ContentDescription.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • This settings/display action is unlabeled for TalkBack, making the toolbar action impossible to identify non-visually. Add the existing localized Settings description.
    app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt:24
  • This query forces every market list into volume order, so the Perpetual → Trending path cannot preserve the API rank as required by perpetualTrendingPreservesApiRankOrder; the mapper receives this already-reordered flow. Persist the API ordinal/rank and query by it for Trending (while keeping a separate volume-sorted query where needed).
        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:345

  • These exact, case-sensitive comparisons regress category filtering for values the existing perps code explicitly supports, such as index, commodity, and fx (see PerpetualContent.kt:654-659 and the removed alias sets in this fragment). Such markets now disappear from their tabs. Normalize categories centrally or restore case-insensitive alias matching.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • This actionable search button has no accessible name, so TalkBack announces it as 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/job/RefreshMarketPageJob.kt:32
  • This replacement for the network-only refresh jobs no longer declares requireNetwork(). While offline, the foreground loop therefore executes eleven doomed requests every 30 seconds instead of letting the queue wait for connectivity, causing unnecessary work and repeated failures. Restore the network constraint.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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

  • The merged market page also uses exact plural category values, so API records using the previously supported aliases (index, commodity, fx) or different casing produce empty category tabs. Restore alias-aware, case-insensitive matching or normalize PerpsMarket.category before mapping.

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