Skip to content

Feat/merge market pages - #6517

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

Feat/merge market pages#6517
SeniorZhai wants to merge 44 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 review requested due to automatic review settings July 31, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Suppressed comments (4)

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

  • This ordering discards the order returned by ranked category endpoints. replaceCategory inserts relations in response order, but trending/featured consumers now receive market-cap order; the previous TradeFragment path used the API list directly, and the equivalent perps DAO deliberately orders by relation rowid. Order by mc.rowid so API-ranked categories remain ranked as returned.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

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

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

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

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Suppressed comments (5)

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

  • This orders every ranked category by the global market-cap rank rather than by the category API's rank. TradeFragment now consumes this flow and the recommendation cards call take(8), so Trending/Top Gainers/Top Losers can show the largest-cap assets instead of the category leaders. Preserve the relation insertion/API order, as PerpsMarketCategoryDao does.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

  • The scan action has no accessible name, so screen-reader users only encounter an unlabeled button. Use the existing localized Scan QR string instead of suppressing the lint warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings/display action has no accessible name, so screen-reader users only encounter an unlabeled button. Use the existing localized Settings string instead of suppressing the lint warning.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • The search action has no accessible name, so screen-reader users only encounter an unlabeled button. Use the existing localized Search string instead of suppressing the lint warning.

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

  • line 40
  • line 52
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:281
  • These favorite and featured syncs are inside the 3-second price polling loop, adding up to 40 extra network requests per minute while this sheet is open. Refresh these membership datasets once when the lifecycle collection starts (or on a substantially slower cadence), and keep only market prices in the fast loop.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Suppressed comments (5)

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

  • This query discards the API category order and reorders every category by global market-cap rank. SwapRecommendedMarketCards takes the first eight observed Trending/Top Gainers/Top Losers entries directly, so those cards will show the largest-cap assets rather than the API-ranked category results; featured recommendations are affected too. Preserve the relation insertion order, as PerpsMarketCategoryDao does.
        ORDER BY CASE WHEN mr.market_cap_rank IS NULL THEN 1 ELSE 0 END,
            CAST(mr.market_cap_rank AS INTEGER) ASC

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

  • The scan button has no accessible name, so screen readers cannot identify its action. Use the existing Scan string instead of suppressing the content-description warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings button is also unlabeled for screen readers. Give it an accessible name rather than suppressing the content-description warning.
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPage.kt:1052
  • When the global-market request fails, the view model marks hasError for MarketPageDataSource.GLOBAL, but this branch ignores that state and always reports “No Markets.” With no cached indicator, network/server failures are therefore misreported instead of showing the network error used by the other tabs. Pass the error state into IndicatorPage and select Network_error here when appropriate.
        indicator == null -> {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center,
            ) {
                Text(
                    text = stringResource(R.string.No_Markets),
                    color = MixinAppTheme.colors.textAssist,

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

  • The search button has no accessible name, and suppressing the lint warning 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 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 72 out of 73 changed files in this pull request and generated 1 comment.

Suppressed comments (9)

app/src/main/res/layout/item_market_list.xml:17

  • This newly clickable favorite control is only 24×24dp, half Android's 48dp minimum touch target, making it difficult for users with motor impairments to activate. Expand the clickable bounds while keeping the star artwork visually sized as needed.
    app/src/main/res/layout/view_home_toolbar.xml:41
  • This scan action has no accessible name, so TalkBack users cannot identify it. Use the existing QR-scan string instead of suppressing the warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings button is exposed without a content description and is therefore unlabeled to screen readers. Use the existing Settings string rather than suppressing the lint warning.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt:1429
  • This replacement drops the previous OLD_VERSION handling. TokenRepository.fetchMarkets consumes failures with empty handlers, so an old-version response now silently leaves recommendations stale instead of showing the required upgrade dialog and exiting the unsupported trade flow. Preserve that typed failure handling when moving the fetch into the repository.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:278
  • This 3-second loop now refreshes favorites and featured markets on every price tick, adding roughly 40 static-list requests per minute while the sheet is open. Local favorite mutations already update the DAO, and featured membership is not tick data; refresh these once on resume or on a substantially slower cadence to avoid unnecessary radio, battery, and backend load.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • Suppressing the content-description warning leaves this search action unlabeled for screen readers. The existing Search string can provide the accessible name.

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/PerpsMarketListAdapter.kt:77
  • The icon is flipped before the request succeeds, but failures do not emit a favorite-ID update and there is no rollback. A failed favorite/unfavorite therefore leaves this recycled row showing the wrong icon indefinitely (while its content description still describes the old state). Rely on the observed DAO state, or explicitly roll back on failure.
    app/src/main/java/one/mixin/android/ui/home/web3/market/MarketPageModels.kt:198
  • These exact comparisons make the merged market page omit perpetuals whose persisted API category uses the aliases already supported by the prior list (index, commodity, or fx), and they also regress case-insensitive matching. Match both canonical and legacy values (or normalize them at persistence time).
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:345
  • Category matching no longer accepts the aliases supported by the previous implementation (stock/stocks, index/indices, commodity/commodities, and forex/fx) and is now case-sensitive. Because the repository persists the API's raw category string without normalization, cached or returned alias values disappear from these filters. Preserve alias and case-insensitive matching, or normalize values when storing them.

.map { request ->
async {
refresh(request.source) {
assetRepo.fetchMarkets(request.category, duration, SPOT_MARKET_LIMIT) != null

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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

Suppressed comments (7)

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

  • This query reorders every API category by market-cap rank. That changes ranked categories such as Trending and Featured from the API order that replaceCategory records; the analogous perps DAO explicitly uses relation rowid to preserve that order. Order by the relation insertion order 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/java/one/mixin/android/db/MarketCategoryDao.kt:62

  • The synchronous category lookup has the same ordering problem: it discards the API's ranked category order and substitutes market-cap rank. Keep this query consistent with the relation insertion order as well.
        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 icon-only scan button has no accessible name; replacing the lint suppression with its existing localized label makes the shared toolbar usable with TalkBack.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • The settings button is also unlabeled for accessibility services because the content-description warning is suppressed.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListAdapter.kt:79
  • The icon is flipped optimistically before the request completes, but a failed request neither rebinds nor restores it. The row can therefore display the opposite favorite state indefinitely; let the favorite-ID flow update the icon only after persistence succeeds.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt:278
  • This 3-second loop now performs three network refreshes instead of one. Favorites and featured recommendations do not need quote-level polling, so this adds roughly 40 requests per minute while the sheet is resumed and also creates races with user favorite updates. Refresh those sources once on resume or on a substantially slower cadence.
    app/src/main/res/layout/view_home_toolbar.xml:29
  • Suppressing the warning leaves this icon-only search button without an accessible name, so TalkBack users cannot identify its action.

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 74 out of 75 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

  • These category flows are consumed directly by the trade recommendation cards, which take the first eight items. Ordering every category by market-cap rank therefore changes the API order for trending/top-gainer/top-loser responses and can select the wrong eight recommendations. Preserve the order recorded by replaceCategory, 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/res/layout/view_home_toolbar.xml:41

  • This actionable scan button has no accessible name, so screen readers announce an unlabeled button. Use the existing scan description instead of suppressing the lint warning.
    app/src/main/res/layout/view_home_toolbar.xml:53
  • This actionable settings button has no accessible name, so screen readers announce an unlabeled button. Use the existing Settings string instead of suppressing the lint warning.
    app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListAdapter.kt:79
  • The icon is changed optimistically before the repository result is known, but the callback only handles success. If the request fails, no favorite flow emission rebinds this row, so it remains visually toggled even though the server and database were not updated. Defer the visual update to the observed favorite IDs (or explicitly roll it back on failure).
    app/src/main/java/one/mixin/android/job/RefreshMarketPageJob.kt:32
  • This job consists entirely of network requests but is allowed to run while offline. The foreground loop will consequently execute and fail the whole refresh every 30 seconds instead of leaving one deduplicated job waiting for connectivity. Add the network constraint, consistent with the other refresh jobs.
            .singleInstanceBy(GROUP),

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

  • This actionable search button has no accessible name, so screen readers announce 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

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.

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