Skip to content

Feat/merge market pages - #6517

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

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

Conversation

@SeniorZhai

Copy link
Copy Markdown
Member

No description provided.

Copilot AI balanced 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 74 out of 75 changed files in this pull request and generated no new comments.

Suppressed comments (4)

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

  • The settings button is exposed as an unlabeled control to accessibility services. Add the existing localized settings label and remove the lint suppression.
    app/src/main/res/layout/view_home_toolbar.xml:41
  • The scan button has no accessible label, leaving TalkBack users unable to identify its action. Use the existing localized QR-scan string rather than suppressing the warning.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This job performs only network-backed refreshes, but unlike the refresh jobs it replaces it has no network constraint. While offline it will run all requests immediately, publish a failed refresh, and be queued again every 30 seconds instead of waiting for connectivity. Mark the job as requiring a network connection.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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

  • The search button suppresses the missing-content-description warning, so screen readers announce an unlabeled button. Give this actionable icon a localized description instead of ignoring the lint check.

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

  • line 40
  • line 52

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 77 out of 78 changed files in this pull request and generated no new comments.

Suppressed comments (6)

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

  • Ordering category rows by global market-cap rank discards the category endpoint's rank order. TradeFragment now observes this flow and the recommendation UI takes the first eight entries, so trending/top recommendations can show different items than the API selected. Preserve insertion/API order here, as the perpetual category DAO already does.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

  • The refresh loop is tied only to lifecycle events, but these bottom-navigation fragments are switched with hide()/show(), which does not send ON_PAUSE. After visiting Markets once, it will therefore keep issuing the full market refresh every 30 seconds while another tab is visible. Stop/start refresh from onHiddenChanged as well (and avoid starting on ON_RESUME while isHidden).
                    DisposableEffect(lifecycleOwner) {
                        val observer =
                            LifecycleEventObserver { _, event ->
                                when (event) {
                                    Lifecycle.Event.ON_RESUME -> viewModel.startRefresh()
                                    Lifecycle.Event.ON_PAUSE -> viewModel.stopRefresh()
                                    else -> Unit

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

  • These exact, case-sensitive comparisons drop category aliases that the existing perpetual UI supports (index/indices, commodity/commodities, and fx/forex). Markets carrying the singular or fx values disappear from the corresponding merged-page tabs. Keep the aliases and case-insensitive matching.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:348
  • The old filter accepted singular/plural category aliases and ignored case, but this replacement only accepts one exact database value. Existing stock, index, commodity, fx, or differently cased records will no longer appear in their tabs; preserve those aliases when filtering.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListAdapter.kt:79
  • This changes the icon optimistically but the callback exposes no failure result to the adapter. If the request fails, the favorites flow does not emit, so the row remains visually toggled (and repeated taps keep using the stale captured isFavored) until some unrelated rebind. Render from favoriteMarketIds, or explicitly roll back on failure.
    app/src/main/res/layout/item_market_list.xml:16
  • The newly clickable favorite control has only a 24×24 dp hit target, half the Android-recommended 48×48 dp minimum. This makes favoriting difficult for users with limited dexterity; enlarge its hit area while keeping the visible star at its current size.

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 78 out of 79 changed files in this pull request and generated no new comments.

Suppressed comments (6)

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

  • A successful response with a null data payload is converted to an empty list here. For all and favorite, the transaction then clears the rank/favorite tables and the refresh job reports success, so a malformed response can erase valid cached market state. Preserve null as a failed refresh; an actual empty list can still clear the cache intentionally.
                val markets = response.data.orEmpty()

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

  • Converting a successful null payload to emptyList() makes syncFavoriteMarkets and syncCategory replace their cached relations with nothing while reporting a successful refresh. Return null for a missing payload so malformed responses retain the last valid cache; a genuine empty list remains distinguishable.
                successBlock = { response ->
                    response.data.orEmpty().map(PerpsMarket::withDefaults)

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

  • The scan action has no accessible label, so assistive technology cannot identify its purpose. Use the existing localized Scan string rather than suppressing the content-description warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings action is exposed as an unlabeled image button to screen readers. Add the existing localized Settings description instead of ignoring the lint check.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This combined refresh job performs only network requests but no longer carries requireNetwork(), unlike both jobs it replaces. When queued offline it runs immediately, marks every source failed, and exits instead of waiting for connectivity; restoring the constraint avoids false error states and unnecessary request bursts.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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

  • The search action has no accessible label; suppressing the lint warning leaves screen-reader users with an unlabeled button. Provide the existing localized Search string as its content description.

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

  • line 40
  • line 52

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 78 out of 79 changed files in this pull request and generated no new comments.

Suppressed comments (8)

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

  • This icon-only scan action has no accessible name, so screen-reader users cannot identify it. Use the existing localized Scan string instead of suppressing the warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings icon is also unlabeled for accessibility services. Replace the suppression with the existing localized Settings description.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:156
  • This exact comparison drops category aliases that are still recognized elsewhere (PerpetualContent.kt:654-659) and were recognized by the replaced implementation. For example, a market with category stock or commodity no longer appears under Stocks or Commodities. Preserve the aliases and case-insensitive matching for every category.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This network-only job lacks requireNetwork(), unlike the jobs it replaces and other refresh jobs. While offline it will run and fan out all refresh requests immediately, then the page loop schedules the same failing work again every 30 seconds. Defer execution until connectivity is available; singleInstanceBy will still coalesce queued refreshes.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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

  • These exact, case-sensitive comparisons regress the aliases the existing perps UI still handles (PerpetualContent.kt:654-659) and that the removed bottom sheet accepted (index/indices, commodity/commodities, and forex/fx). Markets using a singular alias or different case will disappear from the corresponding merged-page tab. Match the supported aliases case-insensitively.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:167
  • Favorites and featured markets are now fetched on every 3-second price refresh, tripling this sheet's polling traffic even though local favorite changes already update the database and featured data is not tick data. Refresh these two once when collection resumes (or on a substantially slower cadence), while keeping only market prices in the tight loop.
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:130
  • The merged page does not track a favorite update as pending or apply an optimistic state. Until the repository flow emits, repeated taps reuse the same stale entry.isFavored value and launch duplicate identical requests; a quick add-then-remove gesture therefore remains added, and concurrent responses can overwrite user intent. Add a per-entry pending/override state (as the new perps bottom sheet does) or disable the action until completion.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • Suppressing the warning leaves the search action unlabeled for TalkBack and other accessibility services. Give this icon-only button its existing localized Search label.

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

  • line 41
  • line 53

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 78 out of 79 changed files in this pull request and generated no new comments.

Suppressed comments (6)

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

  • The Watchlist filter ignores favoriteOverrides, even though the row icon is rendered from isFavorite. During a pending removal the row remains in Watchlist with an unselected star, and during a pending addition it remains absent. Use the effective state here so optimistic updates are applied consistently.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:33
  • The new refresh job performs only network requests but is not constrained to network availability. Offline runs therefore execute immediately, publish every source as failed, and are discarded instead of waiting for connectivity; add the same requireNetwork() constraint used by the other refresh jobs.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

app/src/main/java/one/mixin/android/db/MixinDatabaseMigrations.kt:618

  • This migration adds a new Room table but has no 71→72 migration test. The repository has dedicated migration coverage (including the new perps 6→7 test in this PR), so please add a runMigrationsAndValidate test that starts at version 71 and verifies existing data plus the new table/index.
        val MIGRATION_71_72: Migration =
            object : Migration(71, 72) {
                override fun migrate(db: SupportSQLiteDatabase) {
                    db.execSQL("CREATE TABLE IF NOT EXISTS `market_categories` (`coin_id` TEXT NOT NULL, `category` INTEGER NOT NULL, PRIMARY KEY(`coin_id`, `category`))")
                    db.execSQL("CREATE INDEX IF NOT EXISTS `index_market_categories_category` ON `market_categories` (`category`)")

app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt:1429

  • The previous market refresh path explicitly handled OLD_VERSION by showing the mandatory update dialog. refreshMarketsByCategory now delegates to TokenRepository.fetchMarkets, which silently converts all API failures to null, so an old-client response from these endpoints no longer prompts the user. Preserve the error code through the repository/view-model boundary and retain the update handling.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:53
  • favoriteOverrides is the effective favorite state while a request is pending, but recommendation filtering uses only the persisted IDs. After an optimistic add/remove, a market can therefore simultaneously appear with its new star state and in the recommendation set. Filter through isFavorite so the list is consistent with the rendered state.

This issue also appears on line 152 of the same file.
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketFavoriteIcon.kt:91

  • A trigger reset is currently treated as a new animation because any unequal value plays. Callers reset the counter when the market identity changes (remember(marketId)), while this helper is remembered independently, so navigating to another market after an animation can spuriously replay it. Since all callers increment the counter for events, only a larger value should play.

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 78 out of 79 changed files in this pull request and generated no new comments.

Suppressed comments (5)

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

  • This second category lookup has the same ranking regression: market-cap order replaces the category endpoint's own order. Keep it consistent with observeMarketsByCategory and return rows in relation insertion order.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

  • These category rows are inserted in the API's ranked order, but sorting them by the separate all-market cap rank changes the trending, top_gainers, and top_losers order before the trade page takes its first eight recommendations. Preserve the relation insertion order here, as the perpetual category DAO does, so the category endpoint's ranking survives persistence.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

  • A successful response envelope with a null data payload is treated as an authoritative empty result. The following transaction then clears market-cap ranks, favorites, or category relations, making cached market lists disappear on a malformed response. Return a failed sync for null while still allowing a real empty list to clear a category.
                val markets = response.data.orEmpty()

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

  • Converting a null successful payload to emptyList() makes syncFavoriteMarkets and syncCategory delete their cached relations and report success. Preserve the cache by propagating null as a failed sync; a non-null empty list can still intentionally clear it.
                successBlock = { response ->
                    response.data.orEmpty().map(PerpsMarket::withDefaults)
                },

app/src/main/res/values-zh-rTW/strings.xml:922

  • The new removal flow uses watchlist_remove_desc, but this locale does not define it, so Traditional Chinese users see the English fallback after removing a favorite. Add the localized toast alongside the newly added watchlist strings.

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 83 out of 84 changed files in this pull request and generated no new comments.

Suppressed comments (2)

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

  • This discards the API's category rank and reorders every category by market-cap rank. SwapRecommendedMarketCards then takes only the first eight entries (SwapRecommendedMarketCards.kt:78-103), so Trending and Top Gainers/Losers can show the largest-cap assets instead of the API's top-ranked assets. Preserve the insertion/API order, as the perpetual category DAO does.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

  • The resumed loop now performs three network refreshes every three seconds. Favorites and featured recommendations do not need market-ticker frequency, so keeping them inside this loop triples request volume while the sheet is open; refresh those two once per resume and poll only market prices.

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 83 out of 84 changed files in this pull request and generated no new comments.

Suppressed comments (7)

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

  • This settings action has no accessible name, making the reusable toolbar's third action indistinguishable to screen readers. Use the existing Settings string as its content description.
    app/src/main/res/layout/view_home_toolbar.xml:41
  • This scan action is unlabeled for screen-reader users. Add the existing Scan string as its content description rather than suppressing the warning.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:206
  • The recommendation request can complete after the dialog is dismissed, at which point Fragment.getString throws on the detached fragment. Use the application context to format this asynchronous toast.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • This search button has no accessibility label, so screen readers announce an unlabeled control. Provide the existing Search string as its content description instead of suppressing the lint warning.

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

  • line 40
  • line 52
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:167
  • The 3-second loop now refreshes all markets, favorites, and featured markets every iteration. The latter two calls duplicate market upserts and turn one polling request into three, increasing battery/data use and backend load while the sheet is open. Refresh the relatively static favorite/featured membership once, and keep only price data in the polling loop.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:182
  • This callback is owned by the ViewModel and can finish after the bottom sheet has been dismissed. Calling Fragment.getString then throws because the fragment is detached; format the toast with the application context, as the other market flow does.

This issue also appears on line 206 of the same file.
app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32

  • This job performs only network requests but lacks the network constraint used by the other refresh jobs (for example, RefreshSnapshotsJob.kt:10 and RefreshPerpsPositionsJob.kt:13). Without it, the 30-second UI loop runs all refresh attempts while offline and emits avoidable failure states instead of letting the queue defer work until connectivity returns.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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 87 out of 88 changed files in this pull request and generated no new comments.

Suppressed comments (6)

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

  • Rolling back only the optimistic override does not roll back the favorite icon. MarketListRowFrame still retains the attempted animation intent, and the animation state snaps to that intent’s target until it receives null; therefore a failed add/remove continues to display the wrong favorite state. Clear the row’s intent on failure or make the favorite state carry a reset signal to the row.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetModels.kt:156
  • The new category filter drops aliases that this screen already receives. PerpetualContent.kt:654-659 treats stock/stocks and commodity/commodities as equivalent, and the removed implementation also accepted index, fx, and case variants. With exact equality here, tapping “View all” can produce an empty list for markets shown in the preview. Preserve those aliases (and case-insensitive matching) in this filter.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:167
  • This loop now performs three network refreshes every three seconds. Favorites and featured recommendations are not quote streams, so polling both alongside market prices triples request volume while the sheet is open and wastes battery/data. Refresh those two once on resume, then keep only refreshMarkets() in the interval loop.

This issue also appears on line 178 of the same file.
app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageViewModel.kt:131

  • The favorite requests discard their success result. The row starts a local MarketFavoriteAnimationIntent before invoking this method, and that intent must be cleared to return to the authoritative value. If a request fails, the database state stays unchanged but the icon remains latched in the attempted state. Propagate completion back to the row and clear its animation intent on failure, as the detail page does.
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:217
  • These exact category comparisons repeat the alias regression in the main market page. Existing perps data handling accepts singular values such as index and commodity (and fx for forex), so those markets disappear from the corresponding merged tabs. Match the existing aliases case-insensitively rather than requiring only the plural database values.
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This is a network-only refresh job, but unlike the two jobs it replaces and the other refresh jobs, it no longer declares requireNetwork(). Offline runs therefore execute every request immediately, mark every source failed, and repeat the work every refresh interval instead of waiting for connectivity. Restore the network constraint.
        Params(PRIORITY_UI_HIGH)
            .singleInstanceBy(GROUP),

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.

3 participants