From 96bc40fb27005dc8b7bcb0abcf2357a4ab7aa9e4 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 25 May 2026 20:15:20 +0800 Subject: [PATCH 01/27] feat(perps): add Top Movers card above Trending Show top 8 markets by 24h price change in a 4-column grid above the Trending section. Each item uses a 42dp icon with the max leverage badge anchored to the bottom inside the circle. --- .../home/web3/trade/perps/PerpetualContent.kt | 21 +++ .../ui/home/web3/trade/perps/TopMoversCard.kt | 149 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 171 insertions(+) create mode 100644 app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt index 3dd214ae40..e0ac0d7c5b 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt @@ -102,6 +102,9 @@ fun PerpetualContent( val openPositionsCount = openPositions.size val openPositionsPreview = openPositions.take(3) val marketsPreview = markets.take(3) + val topMoversPreview = remember(markets) { + markets.sortedByDescending { it.changePercent() }.take(8) + } val sourceOrder = remember(markets) { markets.withIndex().associate { it.value.marketId to it.index } } @@ -308,6 +311,24 @@ fun PerpetualContent( } } + if (topMoversPreview.isNotEmpty()) { + Spacer(modifier = Modifier.height(16.dp)) + Column( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(8.dp)) + .cardBackground(Color.Transparent, MixinAppTheme.colors.borderColor) + .padding(vertical = 16.dp) + ) { + TopMoversCard( + markets = topMoversPreview, + quoteColorReversed = quoteColorReversed, + onMarketItemClick = onMarketItemClick, + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) Column( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt new file mode 100644 index 0000000000..404a4d3c3b --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -0,0 +1,149 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.compose.CoilImage +import one.mixin.android.compose.theme.MixinAppTheme +import java.math.BigDecimal + +private const val TOP_MOVERS_COLUMNS = 4 + +@Composable +fun TopMoversCard( + markets: List, + quoteColorReversed: Boolean, + onMarketItemClick: (PerpsMarket) -> Unit, +) { + if (markets.isEmpty()) return + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.perps_top_movers), + fontSize = 14.sp, + color = MixinAppTheme.colors.textPrimary, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + + val rows = markets.chunked(TOP_MOVERS_COLUMNS) + rows.forEachIndexed { index, rowMarkets -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.Top, + ) { + rowMarkets.forEach { market -> + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.TopCenter, + ) { + TopMoverGridItem( + market = market, + quoteColorReversed = quoteColorReversed, + onClick = { onMarketItemClick(market) }, + ) + } + } + repeat(TOP_MOVERS_COLUMNS - rowMarkets.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + if (index != rows.lastIndex) { + Spacer(modifier = Modifier.height(12.dp)) + } + } +} + +@Composable +private fun TopMoverGridItem( + market: PerpsMarket, + quoteColorReversed: Boolean, + onClick: () -> Unit, +) { + val changePercent = market.changePercent() + val isPositive = changePercent >= BigDecimal.ZERO + val changeColor = if (isPositive) { + if (quoteColorReversed) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen + } else { + if (quoteColorReversed) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.walletRed + } + val changeText = formatPerpsSignedPercent(changePercent) + + Column( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick) + .padding(horizontal = 4.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.size(42.dp), + contentAlignment = Alignment.BottomCenter, + ) { + CoilImage( + model = market.iconUrl, + placeholder = R.drawable.ic_avatar_place_holder, + modifier = Modifier + .size(42.dp) + .clip(CircleShape), + ) + Text( + text = "${market.leverage}x", + fontSize = 9.sp, + lineHeight = 10.sp, + color = MixinAppTheme.colors.textPrimary, + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .background(MixinAppTheme.colors.backgroundGrayLight) + .padding(horizontal = 3.dp, vertical = 1.dp), + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = market.tokenSymbol, + fontSize = 13.sp, + color = MixinAppTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = changeText, + fontSize = 12.sp, + color = changeColor, + maxLines = 1, + textAlign = TextAlign.Center, + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b1a3924b6c..c084ff0728 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2471,6 +2471,7 @@ Choose a Country or Region Anonymous Number Trending + Top Movers Market 🚀 Perps %1$s - 📈 Market: %1$s From d51d6bfd8e840242fdbec30bab132f583c5dd209 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 26 May 2026 10:19:19 +0800 Subject: [PATCH 02/27] feat(perps): support initial sort for market list and refine top movers card --- .../ui/home/web3/trade/TradeFragment.kt | 4 ++-- .../android/ui/home/web3/trade/TradePage.kt | 3 ++- .../home/web3/trade/perps/PerpetualContent.kt | 16 +++++++------ ...erpsMarketListBottomSheetDialogFragment.kt | 14 ++++++++++- .../ui/home/web3/trade/perps/TopMoversCard.kt | 23 ++++++++++++++----- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt index b1af27f75d..578280f11d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradeFragment.kt @@ -519,8 +519,8 @@ class TradeFragment : BaseFragment() { onShowMarketList = { isLong -> PerpsMarketListBottomSheetDialogFragment.newInstance(isLong).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) }, - onShowAllMarkets = { initialCategory -> - PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) + onShowAllMarkets = { initialCategory, initialSort -> + PerpsMarketListBottomSheetDialogFragment.newInstance(initialCategory, initialSort).show(parentFragmentManager, PerpsMarketListBottomSheetDialogFragment.TAG) }, onShowAllOpenPositions = { navTo(AllPositionsFragment.newOpenInstance(), AllPositionsFragment.TAG) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt index cf3e7f654c..5b28c70c02 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/TradePage.kt @@ -63,6 +63,7 @@ import one.mixin.android.ui.home.web3.components.OutlinedTab import one.mixin.android.ui.home.web3.components.PageScaffold import one.mixin.android.ui.home.web3.trade.perps.PerpetualContent import one.mixin.android.ui.home.web3.trade.perps.PerpetualViewModel +import one.mixin.android.ui.home.web3.widget.MarketSort import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.vo.WalletCategory import java.math.BigDecimal @@ -103,7 +104,7 @@ fun TradePage( onShowTradingGuide: (Int) -> Unit, onShowHelpBottomSheet: (() -> Unit, () -> Unit) -> Unit, onShowMarketList: (Boolean) -> Unit, - onShowAllMarkets: (String?) -> Unit, + onShowAllMarkets: (String?, MarketSort?) -> Unit, onShowAllOpenPositions: () -> Unit, onShowAllClosedPositions: () -> Unit, onOpenPositionClick: (PerpsPositionItem) -> Unit, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt index e0ac0d7c5b..ac740dd436 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt @@ -55,6 +55,7 @@ import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.extension.putString import one.mixin.android.session.Session +import one.mixin.android.ui.home.web3.widget.MarketSort import one.mixin.android.ui.wallet.alert.components.cardBackground import one.mixin.android.util.analytics.AnalyticsTracker import one.mixin.android.widget.components.MixinButton @@ -68,7 +69,7 @@ private const val CLOSED_POSITION_PREVIEW_LIMIT = 10 fun PerpetualContent( onShowTradingGuide: () -> Unit, onShowMarketList: (isLong: Boolean) -> Unit, - onShowAllMarkets: (String?) -> Unit, + onShowAllMarkets: (String?, MarketSort?) -> Unit, onShowAllOpenPositions: () -> Unit, onShowAllClosedPositions: () -> Unit, onOpenPositionClick: (PerpsPositionItem) -> Unit, @@ -324,6 +325,7 @@ fun PerpetualContent( TopMoversCard( markets = topMoversPreview, quoteColorReversed = quoteColorReversed, + onViewAllClick = { onShowAllMarkets(null, MarketSort.TWENTY_FOUR_HOURS_PERCENTAGE_DESCENDING) }, onMarketItemClick = onMarketItemClick, ) } @@ -345,8 +347,8 @@ fun PerpetualContent( markets = marketsPreview, totalCount = markets.size, quoteColorReversed = quoteColorReversed, - onTitleClick = { onShowAllMarkets(null) }, - onViewAllClick = { onShowAllMarkets(null) }, + onTitleClick = { onShowAllMarkets(null, null) }, + onViewAllClick = { onShowAllMarkets(null, null) }, onMarketItemClick = onMarketItemClick, ) } else if (isLoading) { @@ -394,10 +396,10 @@ fun PerpetualContent( totalCount = stocksMarkets.size, quoteColorReversed = quoteColorReversed, onTitleClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS, null) }, onViewAllClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_STOCKS, null) }, onMarketItemClick = onMarketItemClick, ) @@ -420,10 +422,10 @@ fun PerpetualContent( totalCount = commoditiesMarkets.size, quoteColorReversed = quoteColorReversed, onTitleClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES, null) }, onViewAllClick = { - onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES) + onShowAllMarkets(PerpsMarketListBottomSheetDialogFragment.CATEGORY_COMMODITIES, null) }, onMarketItemClick = onMarketItemClick, ) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt index fc7d45849c..c3f6ad2b8d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketListBottomSheetDialogFragment.kt @@ -57,6 +57,7 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( const val TAG = "PerpsMarketListBottomSheetDialogFragment" private const val ARGS_IS_LONG = "args_is_long" private const val ARGS_INITIAL_CATEGORY = "args_initial_category" + private const val ARGS_INITIAL_SORT = "args_initial_sort" const val CATEGORY_STOCKS = "stocks" const val CATEGORY_COMMODITIES = "commodities" @@ -64,8 +65,12 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( putBoolean(ARGS_IS_LONG, isLong) } - fun newInstance(initialCategory: String? = null) = PerpsMarketListBottomSheetDialogFragment().withArgs { + fun newInstance( + initialCategory: String? = null, + initialSort: MarketSort? = null, + ) = PerpsMarketListBottomSheetDialogFragment().withArgs { initialCategory?.let { putString(ARGS_INITIAL_CATEGORY, it) } + initialSort?.let { putInt(ARGS_INITIAL_SORT, it.value) } } } @@ -87,6 +92,12 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( private val initialCategory by lazy { arguments?.getString(ARGS_INITIAL_CATEGORY) } + private val initialSort by lazy { + arguments + ?.takeIf { it.containsKey(ARGS_INITIAL_SORT) } + ?.getInt(ARGS_INITIAL_SORT) + ?.let { MarketSort.fromValueOrNull(it) } + } private var allMarkets = listOf() private var currentQuery = "" private var currentCategory = MarketCategory.ALL @@ -117,6 +128,7 @@ class PerpsMarketListBottomSheetDialogFragment : MixinBottomSheetDialogFragment( marketRv.adapter = adapter (marketRv.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false applyInitialCategory() + currentSort = initialSort categoryScroll.scrollToCenterCheckedRadio(categoryGroup) searchEt.listener = object : SearchView.OnSearchViewListener { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 404a4d3c3b..6df2321975 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -9,15 +9,18 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -35,6 +38,7 @@ private const val TOP_MOVERS_COLUMNS = 4 fun TopMoversCard( markets: List, quoteColorReversed: Boolean, + onViewAllClick: () -> Unit, onMarketItemClick: (PerpsMarket) -> Unit, ) { if (markets.isEmpty()) return @@ -42,6 +46,7 @@ fun TopMoversCard( Row( modifier = Modifier .fillMaxWidth() + .clickable(onClick = onViewAllClick) .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, @@ -51,6 +56,12 @@ fun TopMoversCard( fontSize = 14.sp, color = MixinAppTheme.colors.textPrimary, ) + Icon( + painter = painterResource(R.drawable.ic_arrow_right), + contentDescription = null, + tint = MixinAppTheme.colors.textAssist, + modifier = Modifier.size(16.dp), + ) } Spacer(modifier = Modifier.height(12.dp)) @@ -101,6 +112,7 @@ private fun TopMoverGridItem( Column( modifier = Modifier + .offset(y = 6.dp) .clip(RoundedCornerShape(8.dp)) .clickable(onClick = onClick) .padding(horizontal = 4.dp, vertical = 8.dp), @@ -119,16 +131,16 @@ private fun TopMoverGridItem( ) Text( text = "${market.leverage}x", - fontSize = 9.sp, - lineHeight = 10.sp, - color = MixinAppTheme.colors.textPrimary, + fontSize = 14.sp, + lineHeight = 16.sp, + color = MixinAppTheme.colors.textAssist, modifier = Modifier .clip(RoundedCornerShape(4.dp)) - .background(MixinAppTheme.colors.backgroundGrayLight) + .background(MixinAppTheme.colors.background) .padding(horizontal = 3.dp, vertical = 1.dp), ) } - Spacer(modifier = Modifier.height(6.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( text = market.tokenSymbol, fontSize = 13.sp, @@ -137,7 +149,6 @@ private fun TopMoverGridItem( overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.height(2.dp)) Text( text = changeText, fontSize = 12.sp, From d86bbdecfb351f29cd7c434c7db79367cab9972a Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 26 May 2026 10:44:03 +0800 Subject: [PATCH 03/27] test(perps): fix instrumentation test crashes and migrate to compose testing --- app/build.gradle.kts | 5 +- .../one/mixin/android/CustomTestRunner.kt | 2 +- .../one/mixin/android/MixinTestApplication.kt | 6 + .../trade/perps/TopMoversScreenshotTest.kt | 108 ++++++++++++++++++ app/src/debug/AndroidManifest.xml | 11 +- .../one/mixin/android/MixinApplication.kt | 19 ++- app/src/staging/AndroidManifest.xml | 11 +- 7 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt create mode 100644 app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5940e486e4..2403eb7d89 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -326,7 +326,7 @@ dependencies { } coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:$desugarJdkLibsVersion") implementation(platform("com.google.firebase:firebase-bom:$firebaseBomVersion")) - implementation("com.google.firebase:firebase-perf") + releaseImplementation("com.google.firebase:firebase-perf") implementation(fileTree(mapOf("include" to listOf("*.aar"), "dir" to "libs"))) implementation("androidx.fragment:fragment-ktx:$fragmentVersion") implementation("androidx.activity:activity-ktx:$activity_version") @@ -438,7 +438,7 @@ dependencies { implementation("com.google.protobuf:protobuf-javalite") { version { - strictly("3.11.0") + strictly("3.25.5") } } @@ -555,6 +555,7 @@ dependencies { } androidTestImplementation("androidx.test.espresso:espresso-idling-resource:$espressoVersion") androidTestImplementation("androidx.test.ext:junit:$androidxJunitVersion") + androidTestImplementation("org.hamcrest:hamcrest:2.2") androidTestImplementation("androidx.fragment:fragment-testing:$fragmentVersion") androidTestImplementation("androidx.navigation:navigation-testing:$navigationVersion") diff --git a/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt b/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt index ebd0c0071b..d1c3c67382 100644 --- a/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt +++ b/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt @@ -11,6 +11,6 @@ class CustomTestRunner : AndroidJUnitRunner() { name: String?, context: Context?, ): Application { - return super.newApplication(cl, HiltTestApplication::class.java.name, context) + return super.newApplication(cl, MixinTestApplication_Application::class.java.name, context) } } diff --git a/app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt b/app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt new file mode 100644 index 0000000000..a12bf871f9 --- /dev/null +++ b/app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt @@ -0,0 +1,6 @@ +package one.mixin.android + +import dagger.hilt.android.testing.CustomTestApplication + +@CustomTestApplication(MixinApplication::class) +interface MixinTestApplication diff --git a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt new file mode 100644 index 0000000000..e43835ccd9 --- /dev/null +++ b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt @@ -0,0 +1,108 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import android.graphics.Bitmap +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import one.mixin.android.HiltTestActivity +import one.mixin.android.api.response.perps.PerpsMarket +import one.mixin.android.compose.theme.MixinAppTheme +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.FileOutputStream + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class TopMoversScreenshotTest { + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule() + + @Test + fun captureTopMoversCard() { + hiltRule.inject() + + composeTestRule.setContent { + MixinAppTheme(darkTheme = false) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + ) { + TopMoversCard( + markets = topMoverMarkets, + quoteColorReversed = false, + onViewAllClick = {}, + onMarketItemClick = {}, + ) + } + } + } + + composeTestRule.onNodeWithText("BTC").assertExists() + + val activity = composeTestRule.activity + val root = activity.window.decorView.rootView + val bitmap = Bitmap.createBitmap(root.width, root.height, Bitmap.Config.ARGB_8888) + val canvas = android.graphics.Canvas(bitmap) + root.draw(canvas) + + val outputDir = File("/sdcard/Android/media/one.mixin.messenger/additional_test_output") + if (!outputDir.exists()) outputDir.mkdirs() + val screenshot = File(outputDir, "top_movers_card.png") + FileOutputStream(screenshot).use { output -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, output) + } + } + + private val topMoverMarkets = listOf( + topMoverMarket("bitcoin", "BTC", "0.1284", 125), + topMoverMarket("ethereum", "ETH", "0.0931", 100), + topMoverMarket("solana", "SOL", "0.0715", 75), + topMoverMarket("dogecoin", "DOGE", "0.0548", 50), + topMoverMarket("sui", "SUI", "-0.0324", 50), + topMoverMarket("hyperliquid", "HYPE", "-0.0417", 25), + topMoverMarket("ripple", "XRP", "-0.0589", 50), + topMoverMarket("toncoin", "TON", "-0.0862", 25), + ) + + private fun topMoverMarket( + id: String, + symbol: String, + change: String, + leverage: Int, + ) = PerpsMarket( + marketId = id, + displaySymbol = "$symbol-PERP", + tokenSymbol = symbol, + quoteSymbol = "USDT", + markPrice = "100.00", + leverage = leverage, + iconUrl = "", + fundingRate = "0.0001", + minAmount = "1", + maxAmount = "100000", + last = "100.00", + volume = "1000000", + high = "120.00", + low = "90.00", + open = "95.00", + change = change, + bidPrice = "99.90", + askPrice = "100.10", + createdAt = "2026-05-26T00:00:00Z", + updatedAt = "2026-05-26T00:00:00Z", + ) +} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index c7a0a1f7fb..5e1eeae16f 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,9 +1,18 @@ - + + + + diff --git a/app/src/main/java/one/mixin/android/MixinApplication.kt b/app/src/main/java/one/mixin/android/MixinApplication.kt index c99a26be56..bd8ddb13b7 100644 --- a/app/src/main/java/one/mixin/android/MixinApplication.kt +++ b/app/src/main/java/one/mixin/android/MixinApplication.kt @@ -40,6 +40,7 @@ import dagger.hilt.components.SingletonComponent import io.reactivex.plugins.RxJavaPlugins import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -146,12 +147,26 @@ open class MixinApplication : private var activityReferences: Int = 0 private var isActivityChangingConfigurations = false - @Inject - @ApplicationScope + @EntryPoint + @InstallIn(SingletonComponent::class) + interface ApplicationScopeEntryPoint { + @one.mixin.android.di.ApplicationScope + fun getApplicationScope(): CoroutineScope + } + + private fun getAppScope(): CoroutineScope { + return try { + EntryPointAccessors.fromApplication(this, ApplicationScopeEntryPoint::class.java).getApplicationScope() + } catch (e: Exception) { + CoroutineScope(Dispatchers.Main + SupervisorJob()) + } + } + lateinit var applicationScope: CoroutineScope override fun onCreate() { super.onCreate() + applicationScope = getAppScope() init() registerActivityLifecycleCallbacks(this) SignalProtocolLoggerProvider.setProvider(MixinSignalProtocolLogger()) diff --git a/app/src/staging/AndroidManifest.xml b/app/src/staging/AndroidManifest.xml index c7a0a1f7fb..5e1eeae16f 100644 --- a/app/src/staging/AndroidManifest.xml +++ b/app/src/staging/AndroidManifest.xml @@ -1,9 +1,18 @@ - + + + + From 6041bb613b408ed21ca6a01480121c8cb658b577 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 26 May 2026 10:44:36 +0800 Subject: [PATCH 04/27] chore: ignore screenshots directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6491007978..a21529ce52 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ agent.md .codex/ .github/copilot-instructions.md .vscode/ +/screenshots/ From e0a1a1a384d0625677b3cc28dd3e1141bbcb5222 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 26 May 2026 10:56:42 +0800 Subject: [PATCH 05/27] test(perps): add dual-theme screenshot test with real market data and fix crashes --- .../trade/perps/TopMoversScreenshotTest.kt | 87 +++++++++++++------ 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt index e43835ccd9..3ba4a7ad67 100644 --- a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt +++ b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt @@ -1,10 +1,16 @@ package one.mixin.android.ui.home.web3.trade.perps import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import android.view.PixelCopy import androidx.activity.compose.setContent +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.Surface import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.test.junit4.createAndroidComposeRule @@ -20,6 +26,8 @@ import org.junit.Test import org.junit.runner.RunWith import java.io.File import java.io.FileOutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) @HiltAndroidTest @@ -31,51 +39,75 @@ class TopMoversScreenshotTest { val composeTestRule = createAndroidComposeRule() @Test - fun captureTopMoversCard() { + fun captureTopMoversCard_Light() { + captureScreenshot(darkTheme = false, fileName = "top_movers_card_light.png") + } + + @Test + fun captureTopMoversCard_Dark() { + captureScreenshot(darkTheme = true, fileName = "top_movers_card_dark.png") + } + + private fun captureScreenshot(darkTheme: Boolean, fileName: String) { hiltRule.inject() composeTestRule.setContent { - MixinAppTheme(darkTheme = false) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp), + MixinAppTheme(darkTheme = darkTheme) { + Surface( + color = MixinAppTheme.colors.background, + modifier = Modifier.fillMaxSize() ) { - TopMoversCard( - markets = topMoverMarkets, - quoteColorReversed = false, - onViewAllClick = {}, - onMarketItemClick = {}, - ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + TopMoversCard( + markets = topMoverMarkets, + quoteColorReversed = false, + onViewAllClick = {}, + onMarketItemClick = {}, + ) + } } } } - composeTestRule.onNodeWithText("BTC").assertExists() + composeTestRule.onNodeWithText("HYPE").assertExists() + + Thread.sleep(5000) val activity = composeTestRule.activity - val root = activity.window.decorView.rootView - val bitmap = Bitmap.createBitmap(root.width, root.height, Bitmap.Config.ARGB_8888) - val canvas = android.graphics.Canvas(bitmap) - root.draw(canvas) + val window = activity.window + val view = window.decorView + val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) + val latch = CountDownLatch(1) + + PixelCopy.request(window, bitmap, { copyResult -> + if (copyResult == PixelCopy.SUCCESS) { + latch.countDown() + } + }, Handler(Looper.getMainLooper())) + + latch.await(5, TimeUnit.SECONDS) val outputDir = File("/sdcard/Android/media/one.mixin.messenger/additional_test_output") if (!outputDir.exists()) outputDir.mkdirs() - val screenshot = File(outputDir, "top_movers_card.png") + val screenshot = File(outputDir, fileName) FileOutputStream(screenshot).use { output -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, output) } } private val topMoverMarkets = listOf( - topMoverMarket("bitcoin", "BTC", "0.1284", 125), - topMoverMarket("ethereum", "ETH", "0.0931", 100), - topMoverMarket("solana", "SOL", "0.0715", 75), - topMoverMarket("dogecoin", "DOGE", "0.0548", 50), - topMoverMarket("sui", "SUI", "-0.0324", 50), - topMoverMarket("hyperliquid", "HYPE", "-0.0417", 25), - topMoverMarket("ripple", "XRP", "-0.0589", 50), - topMoverMarket("toncoin", "TON", "-0.0862", 25), + topMoverMarket("04315ccb-211c-3a12-b28f-60fec2ea69e8", "HYPE", "0.0268", 100, "https://coin-images.coingecko.com/coins/images/50882/large/hyperliquid.jpg?1729431300"), + topMoverMarket("411aae6f-2596-3668-9fca-85f1c4dcd3c6", "TON", "0.0245", 100, "https://images.mixin.one/Qh7MjeINQ6ad68E0FI4iS7bbLGEuF7CZJlTkW1kSAiq8EaFngIZ1tDG0CRHz_hjz8gsiTmHKcdu_0UE1ugmUiHzNwJRm9fjoqJcb=s128"), + topMoverMarket("2c03fc3c-f7c9-39bd-8cdb-ba8a52476dc1", "ALGO", "0.0240", 100, "https://images.mixin.one/-oE4Jsi3aMIkxUPUsvDozyL8D0ccmPkggIdIDu1z8THDQyJcCIsbNwC4amFBiRlkQiLNNjiuMBsNw8sAnehTuI0=s128"), + topMoverMarket("c0349d7b-1b40-3fb7-804a-475abf4aadb7", "BERA", "0.0195", 100, "https://coin-images.coingecko.com/coins/images/25235/large/BERA.png?1738822008"), + topMoverMarket("9aa033e3-5ee4-320c-aa7f-b55b6ccd3a4b", "UNI", "-0.0229", 100, "https://images.mixin.one/Ekf9UzoHhRRfcDLchjfVrRPYZ_71jQt306WcvgwZWwEM2BIGlHcUm_sK3Nw_mjARPwIvNB9xAzAEWJyW86pVuarPu8O5YZ0WwqTo=s128"), + topMoverMarket("32fbceaf-0be1-3039-b721-bc6f638c7f92", "AXS", "-0.0170", 100, "https://images.mixin.one/WiJjvgFGEAHd0Fg8Z5m0eKNpO1f5Frevp2Yyu6KT09zuOZ7-t7tEcfrKVQYPcoJlAxpruILNBD5A05lvXarINxkgRPFGWWwj95Gg=s128"), + topMoverMarket("98bbcbfb-a040-33a2-911f-a7729346b00b", "SUI", "-0.0162", 100, "https://coin-images.mixinpay.com/fe432916-83f8-4d9f-3170-acfa2d1cad00/public"), + topMoverMarket("ced36291-082c-317d-b5b9-4be7e4965dcc", "CRV", "-0.0151", 100, "https://images.mixin.one/ZeFl04CufYhd1_DXRqnqe9xxLEGqVHDCGpDsgfHSnfNH9gYpcKwl2ELYPhLceSjDLO-iglj3pKFpiPN1y2c8QMm0YaURfXvsVH26gQ=s128"), ) private fun topMoverMarket( @@ -83,6 +115,7 @@ class TopMoversScreenshotTest { symbol: String, change: String, leverage: Int, + iconUrl: String, ) = PerpsMarket( marketId = id, displaySymbol = "$symbol-PERP", @@ -90,7 +123,7 @@ class TopMoversScreenshotTest { quoteSymbol = "USDT", markPrice = "100.00", leverage = leverage, - iconUrl = "", + iconUrl = iconUrl, fundingRate = "0.0001", minAmount = "1", maxAmount = "100000", From bfe75ab6f3643b2669b7b892f1c4c9efda919f67 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 26 May 2026 11:02:09 +0800 Subject: [PATCH 06/27] style(perps): refine leverage badge position and item corner radius --- .../android/ui/home/web3/trade/perps/TopMoversCard.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 6df2321975..2581387ba7 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -119,8 +119,8 @@ private fun TopMoverGridItem( horizontalAlignment = Alignment.CenterHorizontally, ) { Box( - modifier = Modifier.size(42.dp), - contentAlignment = Alignment.BottomCenter, + modifier = Modifier.size(width = 42.dp, height = 46.dp), + contentAlignment = Alignment.TopCenter, ) { CoilImage( model = market.iconUrl, @@ -131,10 +131,11 @@ private fun TopMoverGridItem( ) Text( text = "${market.leverage}x", - fontSize = 14.sp, - lineHeight = 16.sp, + fontSize = 12.sp, + lineHeight = 14.sp, color = MixinAppTheme.colors.textAssist, modifier = Modifier + .offset(y = 32.dp) .clip(RoundedCornerShape(4.dp)) .background(MixinAppTheme.colors.background) .padding(horizontal = 3.dp, vertical = 1.dp), From 423219dcc02aed46079f433f21c05f554b45a770 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 11:23:31 +0800 Subject: [PATCH 07/27] fix(perps): show top and bottom movers --- .../home/web3/trade/perps/PerpetualContent.kt | 2 +- .../home/web3/trade/perps/TopMoversPreview.kt | 16 +++++ .../web3/trade/perps/TopMoversPreviewTest.kt | 59 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt create mode 100644 app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt index ac740dd436..12276ac4a3 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt @@ -104,7 +104,7 @@ fun PerpetualContent( val openPositionsPreview = openPositions.take(3) val marketsPreview = markets.take(3) val topMoversPreview = remember(markets) { - markets.sortedByDescending { it.changePercent() }.take(8) + markets.topMoversPreview() } val sourceOrder = remember(markets) { markets.withIndex().associate { it.value.marketId to it.index } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt new file mode 100644 index 0000000000..d6f8c50d12 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreview.kt @@ -0,0 +1,16 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import one.mixin.android.api.response.perps.PerpsMarket + +private const val TOP_MOVERS_GROUP_SIZE = 4 + +fun List.topMoversPreview(): List { + val topMarkets = sortedByDescending { it.changePercent() } + .take(TOP_MOVERS_GROUP_SIZE) + val topMarketIds = topMarkets.map { it.marketId }.toSet() + val bottomMarkets = sortedBy { it.changePercent() } + .filterNot { it.marketId in topMarketIds } + .take(TOP_MOVERS_GROUP_SIZE) + + return topMarkets + bottomMarkets +} diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt new file mode 100644 index 0000000000..dec86ee393 --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt @@ -0,0 +1,59 @@ +package one.mixin.android.ui.home.web3.trade.perps + +import one.mixin.android.api.response.perps.PerpsMarket +import kotlin.test.assertEquals +import org.junit.Test + +class TopMoversPreviewTest { + @Test + fun topMoversPreviewUsesTopFourAndBottomFourMarkets() { + val markets = listOf( + market("a", "0.01"), + market("b", "0.08"), + market("c", "-0.04"), + market("d", "0.03"), + market("e", "-0.09"), + market("f", "0.05"), + market("g", "-0.02"), + market("h", "0.13"), + market("i", "-0.12"), + market("j", "0.07"), + ) + + val result = markets.topMoversPreview().map { it.marketId } + + assertEquals( + listOf("h", "b", "j", "f", "i", "e", "c", "g"), + result, + ) + } + + private fun market( + marketId: String, + change: String, + ) = PerpsMarket( + marketId = marketId, + displaySymbol = marketId, + tokenSymbol = marketId, + quoteSymbol = "USDT", + markPrice = "1", + priceScale = 2, + leverage = 10, + iconUrl = "", + category = "", + tags = emptyList(), + fundingRate = "0", + minAmount = "0", + maxAmount = "0", + last = "1", + volume = "0", + high = "1", + low = "1", + open = "1", + change = change, + bidPrice = "1", + askPrice = "1", + createdAt = "", + updatedAt = "", + ) +} From 3d83e57173f88574a46db450a94cece3946cda03 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 14:19:22 +0800 Subject: [PATCH 08/27] feat(perps): update top movers card text styling --- .../mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 2581387ba7..81d38ff654 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -134,6 +135,7 @@ private fun TopMoverGridItem( fontSize = 12.sp, lineHeight = 14.sp, color = MixinAppTheme.colors.textAssist, + fontWeight = FontWeight.W500, modifier = Modifier .offset(y = 32.dp) .clip(RoundedCornerShape(4.dp)) @@ -144,7 +146,8 @@ private fun TopMoverGridItem( Spacer(modifier = Modifier.height(4.dp)) Text( text = market.tokenSymbol, - fontSize = 13.sp, + fontSize = 14.sp, + lineHeight = 18.sp, color = MixinAppTheme.colors.textPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis, From b9a44b251e0a6c66c4b8032fcc86d461f57712f0 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 14:39:35 +0800 Subject: [PATCH 09/27] fix(perps): add i18n for top movers label --- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2b0d4cf1e9..4c73ed9469 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2502,4 +2502,5 @@ 收益率 邀请好友一起交易 通过此链接赚取 %1$s 分佣 + 涨跌榜 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 993155e680..3a21fe3828 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1104,4 +1104,5 @@ 觸發止損:控制虧損 止盈 止損 + 漲跌榜 From 49543a9849e4490e87754c320acd026e7d529245 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 15:08:25 +0800 Subject: [PATCH 10/27] feat(perps): format large percent values with K suffix - Add K suffix formatting for percent values >= 1000% (e.g., 1.5K%, 10K%) - Add unit tests for large percent formatting - Update screenshot test data to show 1.5K% example --- .../home/web3/trade/perps/TopMoversScreenshotTest.kt | 2 +- .../android/ui/home/web3/trade/perps/PerpsFormat.kt | 8 ++++++++ .../android/ui/home/web3/trade/perps/TopMoversCard.kt | 2 +- .../ui/home/web3/trade/perps/TopMoversPreviewTest.kt | 11 +++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt index 3ba4a7ad67..69050112e8 100644 --- a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt +++ b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt @@ -100,7 +100,7 @@ class TopMoversScreenshotTest { } private val topMoverMarkets = listOf( - topMoverMarket("04315ccb-211c-3a12-b28f-60fec2ea69e8", "HYPE", "0.0268", 100, "https://coin-images.coingecko.com/coins/images/50882/large/hyperliquid.jpg?1729431300"), + topMoverMarket("04315ccb-211c-3a12-b28f-60fec2ea69e8", "HYPE", "15.00", 100, "https://coin-images.coingecko.com/coins/images/50882/large/hyperliquid.jpg?1729431300"), topMoverMarket("411aae6f-2596-3668-9fca-85f1c4dcd3c6", "TON", "0.0245", 100, "https://images.mixin.one/Qh7MjeINQ6ad68E0FI4iS7bbLGEuF7CZJlTkW1kSAiq8EaFngIZ1tDG0CRHz_hjz8gsiTmHKcdu_0UE1ugmUiHzNwJRm9fjoqJcb=s128"), topMoverMarket("2c03fc3c-f7c9-39bd-8cdb-ba8a52476dc1", "ALGO", "0.0240", 100, "https://images.mixin.one/-oE4Jsi3aMIkxUPUsvDozyL8D0ccmPkggIdIDu1z8THDQyJcCIsbNwC4amFBiRlkQiLNNjiuMBsNw8sAnehTuI0=s128"), topMoverMarket("c0349d7b-1b40-3fb7-804a-475abf4aadb7", "BERA", "0.0195", 100, "https://coin-images.coingecko.com/coins/images/25235/large/BERA.png?1738822008"), diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt index 75575cda7c..68ecfb42b6 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsFormat.kt @@ -143,6 +143,14 @@ fun formatPerpsSignedPercent(value: Double, withSign: Boolean = true): String { private fun formatPerpsPercentDecimal(value: BigDecimal): String { val safeValue = value.abs() + if (safeValue >= BigDecimal(1000)) { + val thousands = safeValue.divide(BigDecimal(1000), 1, RoundingMode.FLOOR) + return if (thousands.stripTrailingZeros().scale() <= 0) { + "${thousands.toBigInteger()}K" + } else { + "${thousands.stripTrailingZeros().toPlainString()}K" + } + } val scaled = safeValue.setScale(2, RoundingMode.FLOOR) if (scaled.compareTo(BigDecimal.ZERO) == 0) return "0.0" return scaled.stripTrailingZeros().toPlainString() diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 81d38ff654..e96d4c9efa 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -155,7 +155,7 @@ private fun TopMoverGridItem( ) Text( text = changeText, - fontSize = 12.sp, + fontSize = 14.sp, color = changeColor, maxLines = 1, textAlign = TextAlign.Center, diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt index dec86ee393..7c0b57fc7d 100644 --- a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt +++ b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt @@ -1,6 +1,7 @@ package one.mixin.android.ui.home.web3.trade.perps import one.mixin.android.api.response.perps.PerpsMarket +import java.math.BigDecimal import kotlin.test.assertEquals import org.junit.Test @@ -28,6 +29,16 @@ class TopMoversPreviewTest { ) } + @Test + fun formatPerpsSignedPercent_formatsLargeValuesWithK() { + assertEquals("+1.5K%", formatPerpsSignedPercent(BigDecimal(1500))) + assertEquals("+1K%", formatPerpsSignedPercent(BigDecimal(1000))) + assertEquals("-2.3K%", formatPerpsSignedPercent(BigDecimal(-2300))) + assertEquals("+500%", formatPerpsSignedPercent(BigDecimal(500))) + assertEquals("+999.99%", formatPerpsSignedPercent(BigDecimal(999.99))) + assertEquals("+10.5K%", formatPerpsSignedPercent(BigDecimal(10500))) + } + private fun market( marketId: String, change: String, From 976f643306d8b6ffffaadc7408eb190892e65e59 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 15:23:23 +0800 Subject: [PATCH 11/27] fix(build): resolve protobuf-javalite dependency conflict - Update protobuf-javalite from 3.25.5/3.11.0 to 4.29.3 - Add google/protobuf/descriptor.proto to packaging excludes --- app/build.gradle.kts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3c3bd5901..b8e2603d82 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,6 +153,7 @@ android { excludes += "META-INF/DISCLAIMER" excludes += "META-INF/NOTICE.md" excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "google/protobuf/descriptor.proto" } jniLibs { useLegacyPackaging = true @@ -439,7 +440,7 @@ dependencies { implementation("com.google.protobuf:protobuf-javalite") { version { - strictly("3.25.5") + strictly("4.29.3") } } @@ -570,7 +571,7 @@ dependencies { testImplementation("com.google.protobuf:protobuf-javalite") { version { - strictly("3.11.0") + strictly("4.29.3") } } From a7702bb9753f99cc5932d3bc2cbaa128006a948a Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 15:38:37 +0800 Subject: [PATCH 12/27] feat(perps): add autoSize to top movers item text --- .../ui/home/web3/trade/perps/TopMoversCard.kt | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index e96d4c9efa..851268d798 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -14,6 +14,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -22,9 +24,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.R @@ -130,12 +132,20 @@ private fun TopMoverGridItem( .size(42.dp) .clip(CircleShape), ) - Text( + BasicText( text = "${market.leverage}x", - fontSize = 12.sp, - lineHeight = 14.sp, - color = MixinAppTheme.colors.textAssist, - fontWeight = FontWeight.W500, + style = TextStyle( + fontSize = 12.sp, + lineHeight = 14.sp, + color = MixinAppTheme.colors.textAssist, + fontWeight = FontWeight.W500, + ), + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 12.sp, + stepSize = 0.5.sp + ), modifier = Modifier .offset(y = 32.dp) .clip(RoundedCornerShape(4.dp)) @@ -144,21 +154,34 @@ private fun TopMoverGridItem( ) } Spacer(modifier = Modifier.height(4.dp)) - Text( + BasicText( text = market.tokenSymbol, - fontSize = 14.sp, - lineHeight = 18.sp, - color = MixinAppTheme.colors.textPrimary, + style = TextStyle( + fontSize = 14.sp, + lineHeight = 18.sp, + color = MixinAppTheme.colors.textPrimary, + textAlign = TextAlign.Center, + ), maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 14.sp, + stepSize = 0.5.sp + ), ) - Text( + BasicText( text = changeText, - fontSize = 14.sp, - color = changeColor, + style = TextStyle( + fontSize = 14.sp, + color = changeColor, + textAlign = TextAlign.Center, + ), maxLines = 1, - textAlign = TextAlign.Center, + autoSize = TextAutoSize.StepBased( + minFontSize = 8.sp, + maxFontSize = 14.sp, + stepSize = 0.5.sp + ), ) } } From 7ba5a51846f9877b2067f636b3c31bc74bf502b8 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 16:17:56 +0800 Subject: [PATCH 13/27] fix(perps): stabilize position list item keys --- .../home/web3/trade/perps/AllPositionsPage.kt | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt index e5aafdfca9..c7e9c56608 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt @@ -22,6 +22,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -45,6 +46,8 @@ import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.isActive @@ -306,7 +309,7 @@ private fun LazyListScope.openPositionItems( onPositionClick: (PerpsPositionItem) -> Unit, ) { if (positions.itemCount == 0) return - item { + item(key = "open_positions_card") { Column( modifier = Modifier .fillMaxWidth() @@ -319,10 +322,12 @@ private fun LazyListScope.openPositionItems( ) { for (index in 0 until positions.itemCount) { val position = positions[index] ?: continue - OpenPositionItem( - position = position, - onClick = { onPositionClick(position) }, - ) + key(position.positionId) { + OpenPositionItem( + position = position, + onClick = { onPositionClick(position) }, + ) + } } } } @@ -332,7 +337,13 @@ private fun LazyListScope.closedPositionItems( positions: LazyPagingItems, onPositionClick: (PerpsOrderItem) -> Unit, ) { - items(count = positions.itemCount) { index -> + items( + count = positions.itemCount, + key = positions.itemKey { it.orderId }, + contentType = positions.itemContentType { order -> + if (order.orderType == PerpsOrder.TYPE_CLOSE) "close" else "open" + }, + ) { index -> val order = positions[index] ?: return@items if (order.orderType == PerpsOrder.TYPE_CLOSE) { ClosedActivityItem( From d599476cfffb685ce879ec96fc2eb7bf582c7558 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 16:52:12 +0800 Subject: [PATCH 14/27] fix(perps): improve positions list rendering --- .../api/response/perps/PerpsOrderItem.kt | 2 ++ .../api/response/perps/PerpsPositionItem.kt | 2 ++ .../home/web3/trade/perps/AllPositionsPage.kt | 34 +++++++++++-------- .../ui/home/web3/trade/perps/TopMoversCard.kt | 1 + 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt index 8800b4aaca..1a0ff3b534 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt @@ -1,10 +1,12 @@ package one.mixin.android.api.response.perps import android.os.Parcelable +import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize +@Immutable @Parcelize data class PerpsOrderItem( @SerializedName("order_id") diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt index 321301ea17..a70da95e91 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPositionItem.kt @@ -1,10 +1,12 @@ package one.mixin.android.api.response.perps import android.os.Parcelable +import androidx.compose.runtime.Immutable import androidx.room.ColumnInfo import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize +@Immutable @Parcelize data class PerpsPositionItem( @SerializedName("position_id") diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt index c7e9c56608..ec6d160048 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon @@ -22,7 +23,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -309,26 +309,32 @@ private fun LazyListScope.openPositionItems( onPositionClick: (PerpsPositionItem) -> Unit, ) { if (positions.itemCount == 0) return - item(key = "open_positions_card") { - Column( + items( + count = positions.itemCount, + key = positions.itemKey { it.positionId }, + contentType = positions.itemContentType { "open_position" }, + ) { index -> + val position = positions[index] ?: return@items + val shape = when { + positions.itemCount == 1 -> RoundedCornerShape(8.dp) + index == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + index == positions.itemCount - 1 -> RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp) + else -> RoundedCornerShape(0.dp) + } + Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) + .clip(shape) .cardBackground( backgroundColor = MixinAppTheme.colors.background, borderColor = MixinAppTheme.colors.borderColor, + cornerRadius = if (index == 0 || index == positions.itemCount - 1) 8.dp else 0.dp, ) - .padding(vertical = 8.dp) ) { - for (index in 0 until positions.itemCount) { - val position = positions[index] ?: continue - key(position.positionId) { - OpenPositionItem( - position = position, - onClick = { onPositionClick(position) }, - ) - } - } + OpenPositionItem( + position = position, + onClick = { onPositionClick(position) }, + ) } } } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 851268d798..92ee978bae 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -169,6 +169,7 @@ private fun TopMoverGridItem( stepSize = 0.5.sp ), ) + Spacer(modifier = Modifier.height(2.dp)) BasicText( text = changeText, style = TextStyle( From 6cb03a8e8dd56bb798378c9bad2e5ece5a2a89f4 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 17:09:02 +0800 Subject: [PATCH 15/27] fix(perps): update share link type --- .../link/LinkBottomSheetDialogFragment.kt | 2 +- .../home/web3/trade/perps/PerpetualContent.kt | 24 +++++++++++++++++++ .../perps/PerpsPositionShareBottomFragment.kt | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt index 3becd50957..20dbcb2beb 100644 --- a/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/conversation/link/LinkBottomSheetDialogFragment.kt @@ -1107,7 +1107,7 @@ class LinkBottomSheetDialogFragment : SchemeBottomSheet() { private suspend fun handleTradeScheme(uri: Uri) { val type = uri.getQueryParameter("type") - if (type.equals("perps", true)) { + if (type.equals("perps", true) || type.equals("perpetual", true)) { val marketId = uri.getQueryParameter("market") if (marketId.isNullOrBlank() || !marketId.isUUID()) { showError(R.string.Invalid_payment_link) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt index 12276ac4a3..8a208c43bf 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt @@ -1,5 +1,7 @@ package one.mixin.android.ui.home.web3.trade.perps +import android.graphics.BlurMaskFilter +import android.graphics.Paint import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -30,7 +32,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -318,6 +323,7 @@ fun PerpetualContent( Modifier .fillMaxWidth() .wrapContentHeight() + .topMoversCardShadow() .clip(RoundedCornerShape(8.dp)) .cardBackground(Color.Transparent, MixinAppTheme.colors.borderColor) .padding(vertical = 16.dp) @@ -661,6 +667,24 @@ private fun calculatePnlPercent( .toDouble() } +private fun Modifier.topMoversCardShadow(): Modifier = drawBehind { + val blur = 2.dp.toPx() + val offsetY = (-1).dp.toPx() + val radius = 8.dp.toPx() + val paint = Paint().apply { + color = Color.Black.copy(alpha = 0.04f).toArgb() + maskFilter = BlurMaskFilter(blur, BlurMaskFilter.Blur.NORMAL) + } + drawContext.canvas.nativeCanvas.drawRoundRect( + 0f, + offsetY, + size.width, + size.height + offsetY, + radius, + radius, + paint, + ) +} @Composable private fun ViewAllAction(onClick: () -> Unit) { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt index 8a4d7bd2a1..eb4a6e1e70 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt @@ -424,7 +424,7 @@ class PerpsPositionShareBottomFragment : MixinBottomSheetDialogFragment() { } private fun buildPerpsAppCardMessage(): ForwardMessage { - val action = "${Constants.Scheme.HTTPS_TRADE}?type=perpetual&market=${shareData.marketId}" + val action = "${Constants.Scheme.HTTPS_TRADE}?type=perps&market=${shareData.marketId}" val side = if (shareData.side.equals("long", ignoreCase = true)) getString(R.string.Long) else getString(R.string.Short) val market = shareData.displaySymbol.ifBlank { shareData.tokenSymbol } val title = getString(R.string.perps_share_card_title, shareData.tokenSymbol) From 8c242d76432c4715cfcce391b469c9ec13b4b4dd Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 17:19:50 +0800 Subject: [PATCH 16/27] fix(perps): add shadow to top mover leverage label --- .../home/web3/trade/perps/PerpetualContent.kt | 25 ------------------ .../ui/home/web3/trade/perps/TopMoversCard.kt | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt index 8a208c43bf..80893547b8 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualContent.kt @@ -1,7 +1,5 @@ package one.mixin.android.ui.home.web3.trade.perps -import android.graphics.BlurMaskFilter -import android.graphics.Paint import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -32,10 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -323,7 +318,6 @@ fun PerpetualContent( Modifier .fillMaxWidth() .wrapContentHeight() - .topMoversCardShadow() .clip(RoundedCornerShape(8.dp)) .cardBackground(Color.Transparent, MixinAppTheme.colors.borderColor) .padding(vertical = 16.dp) @@ -667,25 +661,6 @@ private fun calculatePnlPercent( .toDouble() } -private fun Modifier.topMoversCardShadow(): Modifier = drawBehind { - val blur = 2.dp.toPx() - val offsetY = (-1).dp.toPx() - val radius = 8.dp.toPx() - val paint = Paint().apply { - color = Color.Black.copy(alpha = 0.04f).toArgb() - maskFilter = BlurMaskFilter(blur, BlurMaskFilter.Blur.NORMAL) - } - drawContext.canvas.nativeCanvas.drawRoundRect( - 0f, - offsetY, - size.width, - size.height + offsetY, - radius, - radius, - paint, - ) -} - @Composable private fun ViewAllAction(onClick: () -> Unit) { Row( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 92ee978bae..d4c040d4af 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -1,5 +1,7 @@ package one.mixin.android.ui.home.web3.trade.perps +import android.graphics.BlurMaskFilter +import android.graphics.Paint import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -22,9 +24,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -148,6 +154,7 @@ private fun TopMoverGridItem( ), modifier = Modifier .offset(y = 32.dp) + .topMoverLeverageShadow() .clip(RoundedCornerShape(4.dp)) .background(MixinAppTheme.colors.background) .padding(horizontal = 3.dp, vertical = 1.dp), @@ -186,3 +193,22 @@ private fun TopMoverGridItem( ) } } + +private fun Modifier.topMoverLeverageShadow(): Modifier = drawBehind { + val blur = 2.dp.toPx() + val offsetY = (-1).dp.toPx() + val radius = 4.dp.toPx() + val paint = Paint().apply { + color = Color.Black.copy(alpha = 0.04f).toArgb() + maskFilter = BlurMaskFilter(blur, BlurMaskFilter.Blur.NORMAL) + } + drawContext.canvas.nativeCanvas.drawRoundRect( + 0f, + offsetY, + size.width, + size.height + offsetY, + radius, + radius, + paint, + ) +} From 0f4ab2b94f4b17ee9c7c1c5e0ecf9bb25e001107 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 18:21:48 +0800 Subject: [PATCH 17/27] chore(test): revert protobuf test changes --- .gitignore | 1 - app/build.gradle.kts | 8 +- .../one/mixin/android/CustomTestRunner.kt | 2 +- .../one/mixin/android/MixinTestApplication.kt | 6 - .../trade/perps/TopMoversScreenshotTest.kt | 141 ------------------ app/src/debug/AndroidManifest.xml | 11 +- app/src/staging/AndroidManifest.xml | 11 +- .../web3/trade/perps/TopMoversPreviewTest.kt | 70 --------- 8 files changed, 6 insertions(+), 244 deletions(-) delete mode 100644 app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt delete mode 100644 app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt delete mode 100644 app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt diff --git a/.gitignore b/.gitignore index a21529ce52..6491007978 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,3 @@ agent.md .codex/ .github/copilot-instructions.md .vscode/ -/screenshots/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1ca0c4ae2..e8319f684d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,7 +153,6 @@ android { excludes += "META-INF/DISCLAIMER" excludes += "META-INF/NOTICE.md" excludes += "/META-INF/{AL2.0,LGPL2.1}" - excludes += "google/protobuf/descriptor.proto" } jniLibs { useLegacyPackaging = true @@ -327,7 +326,7 @@ dependencies { } coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:$desugarJdkLibsVersion") implementation(platform("com.google.firebase:firebase-bom:$firebaseBomVersion")) - releaseImplementation("com.google.firebase:firebase-perf") + implementation("com.google.firebase:firebase-perf") implementation(fileTree(mapOf("include" to listOf("*.aar"), "dir" to "libs"))) implementation("androidx.fragment:fragment-ktx:$fragmentVersion") implementation("androidx.activity:activity-ktx:$activity_version") @@ -440,7 +439,7 @@ dependencies { implementation("com.google.protobuf:protobuf-javalite") { version { - strictly("4.29.3") + strictly("3.11.0") } } @@ -557,7 +556,6 @@ dependencies { } androidTestImplementation("androidx.test.espresso:espresso-idling-resource:$espressoVersion") androidTestImplementation("androidx.test.ext:junit:$androidxJunitVersion") - androidTestImplementation("org.hamcrest:hamcrest:2.2") androidTestImplementation("androidx.fragment:fragment-testing:$fragmentVersion") androidTestImplementation("androidx.navigation:navigation-testing:$navigationVersion") @@ -571,7 +569,7 @@ dependencies { testImplementation("com.google.protobuf:protobuf-javalite") { version { - strictly("4.29.3") + strictly("3.11.0") } } diff --git a/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt b/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt index d1c3c67382..ebd0c0071b 100644 --- a/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt +++ b/app/src/androidTest/java/one/mixin/android/CustomTestRunner.kt @@ -11,6 +11,6 @@ class CustomTestRunner : AndroidJUnitRunner() { name: String?, context: Context?, ): Application { - return super.newApplication(cl, MixinTestApplication_Application::class.java.name, context) + return super.newApplication(cl, HiltTestApplication::class.java.name, context) } } diff --git a/app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt b/app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt deleted file mode 100644 index a12bf871f9..0000000000 --- a/app/src/androidTest/java/one/mixin/android/MixinTestApplication.kt +++ /dev/null @@ -1,6 +0,0 @@ -package one.mixin.android - -import dagger.hilt.android.testing.CustomTestApplication - -@CustomTestApplication(MixinApplication::class) -interface MixinTestApplication diff --git a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt b/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt deleted file mode 100644 index 69050112e8..0000000000 --- a/app/src/androidTest/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversScreenshotTest.kt +++ /dev/null @@ -1,141 +0,0 @@ -package one.mixin.android.ui.home.web3.trade.perps - -import android.graphics.Bitmap -import android.os.Handler -import android.os.Looper -import android.view.PixelCopy -import androidx.activity.compose.setContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Surface -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.test.ext.junit.runners.AndroidJUnit4 -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import one.mixin.android.HiltTestActivity -import one.mixin.android.api.response.perps.PerpsMarket -import one.mixin.android.compose.theme.MixinAppTheme -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import java.io.File -import java.io.FileOutputStream -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -@RunWith(AndroidJUnit4::class) -@HiltAndroidTest -class TopMoversScreenshotTest { - @get:Rule(order = 0) - val hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - val composeTestRule = createAndroidComposeRule() - - @Test - fun captureTopMoversCard_Light() { - captureScreenshot(darkTheme = false, fileName = "top_movers_card_light.png") - } - - @Test - fun captureTopMoversCard_Dark() { - captureScreenshot(darkTheme = true, fileName = "top_movers_card_dark.png") - } - - private fun captureScreenshot(darkTheme: Boolean, fileName: String) { - hiltRule.inject() - - composeTestRule.setContent { - MixinAppTheme(darkTheme = darkTheme) { - Surface( - color = MixinAppTheme.colors.background, - modifier = Modifier.fillMaxSize() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - TopMoversCard( - markets = topMoverMarkets, - quoteColorReversed = false, - onViewAllClick = {}, - onMarketItemClick = {}, - ) - } - } - } - } - - composeTestRule.onNodeWithText("HYPE").assertExists() - - Thread.sleep(5000) - - val activity = composeTestRule.activity - val window = activity.window - val view = window.decorView - val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) - val latch = CountDownLatch(1) - - PixelCopy.request(window, bitmap, { copyResult -> - if (copyResult == PixelCopy.SUCCESS) { - latch.countDown() - } - }, Handler(Looper.getMainLooper())) - - latch.await(5, TimeUnit.SECONDS) - - val outputDir = File("/sdcard/Android/media/one.mixin.messenger/additional_test_output") - if (!outputDir.exists()) outputDir.mkdirs() - val screenshot = File(outputDir, fileName) - FileOutputStream(screenshot).use { output -> - bitmap.compress(Bitmap.CompressFormat.PNG, 100, output) - } - } - - private val topMoverMarkets = listOf( - topMoverMarket("04315ccb-211c-3a12-b28f-60fec2ea69e8", "HYPE", "15.00", 100, "https://coin-images.coingecko.com/coins/images/50882/large/hyperliquid.jpg?1729431300"), - topMoverMarket("411aae6f-2596-3668-9fca-85f1c4dcd3c6", "TON", "0.0245", 100, "https://images.mixin.one/Qh7MjeINQ6ad68E0FI4iS7bbLGEuF7CZJlTkW1kSAiq8EaFngIZ1tDG0CRHz_hjz8gsiTmHKcdu_0UE1ugmUiHzNwJRm9fjoqJcb=s128"), - topMoverMarket("2c03fc3c-f7c9-39bd-8cdb-ba8a52476dc1", "ALGO", "0.0240", 100, "https://images.mixin.one/-oE4Jsi3aMIkxUPUsvDozyL8D0ccmPkggIdIDu1z8THDQyJcCIsbNwC4amFBiRlkQiLNNjiuMBsNw8sAnehTuI0=s128"), - topMoverMarket("c0349d7b-1b40-3fb7-804a-475abf4aadb7", "BERA", "0.0195", 100, "https://coin-images.coingecko.com/coins/images/25235/large/BERA.png?1738822008"), - topMoverMarket("9aa033e3-5ee4-320c-aa7f-b55b6ccd3a4b", "UNI", "-0.0229", 100, "https://images.mixin.one/Ekf9UzoHhRRfcDLchjfVrRPYZ_71jQt306WcvgwZWwEM2BIGlHcUm_sK3Nw_mjARPwIvNB9xAzAEWJyW86pVuarPu8O5YZ0WwqTo=s128"), - topMoverMarket("32fbceaf-0be1-3039-b721-bc6f638c7f92", "AXS", "-0.0170", 100, "https://images.mixin.one/WiJjvgFGEAHd0Fg8Z5m0eKNpO1f5Frevp2Yyu6KT09zuOZ7-t7tEcfrKVQYPcoJlAxpruILNBD5A05lvXarINxkgRPFGWWwj95Gg=s128"), - topMoverMarket("98bbcbfb-a040-33a2-911f-a7729346b00b", "SUI", "-0.0162", 100, "https://coin-images.mixinpay.com/fe432916-83f8-4d9f-3170-acfa2d1cad00/public"), - topMoverMarket("ced36291-082c-317d-b5b9-4be7e4965dcc", "CRV", "-0.0151", 100, "https://images.mixin.one/ZeFl04CufYhd1_DXRqnqe9xxLEGqVHDCGpDsgfHSnfNH9gYpcKwl2ELYPhLceSjDLO-iglj3pKFpiPN1y2c8QMm0YaURfXvsVH26gQ=s128"), - ) - - private fun topMoverMarket( - id: String, - symbol: String, - change: String, - leverage: Int, - iconUrl: String, - ) = PerpsMarket( - marketId = id, - displaySymbol = "$symbol-PERP", - tokenSymbol = symbol, - quoteSymbol = "USDT", - markPrice = "100.00", - leverage = leverage, - iconUrl = iconUrl, - fundingRate = "0.0001", - minAmount = "1", - maxAmount = "100000", - last = "100.00", - volume = "1000000", - high = "120.00", - low = "90.00", - open = "95.00", - change = change, - bidPrice = "99.90", - askPrice = "100.10", - createdAt = "2026-05-26T00:00:00Z", - updatedAt = "2026-05-26T00:00:00Z", - ) -} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 5e1eeae16f..c7a0a1f7fb 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,18 +1,9 @@ - + - - - diff --git a/app/src/staging/AndroidManifest.xml b/app/src/staging/AndroidManifest.xml index 5e1eeae16f..c7a0a1f7fb 100644 --- a/app/src/staging/AndroidManifest.xml +++ b/app/src/staging/AndroidManifest.xml @@ -1,18 +1,9 @@ - + - - - diff --git a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt b/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt deleted file mode 100644 index 7c0b57fc7d..0000000000 --- a/app/src/test/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversPreviewTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package one.mixin.android.ui.home.web3.trade.perps - -import one.mixin.android.api.response.perps.PerpsMarket -import java.math.BigDecimal -import kotlin.test.assertEquals -import org.junit.Test - -class TopMoversPreviewTest { - @Test - fun topMoversPreviewUsesTopFourAndBottomFourMarkets() { - val markets = listOf( - market("a", "0.01"), - market("b", "0.08"), - market("c", "-0.04"), - market("d", "0.03"), - market("e", "-0.09"), - market("f", "0.05"), - market("g", "-0.02"), - market("h", "0.13"), - market("i", "-0.12"), - market("j", "0.07"), - ) - - val result = markets.topMoversPreview().map { it.marketId } - - assertEquals( - listOf("h", "b", "j", "f", "i", "e", "c", "g"), - result, - ) - } - - @Test - fun formatPerpsSignedPercent_formatsLargeValuesWithK() { - assertEquals("+1.5K%", formatPerpsSignedPercent(BigDecimal(1500))) - assertEquals("+1K%", formatPerpsSignedPercent(BigDecimal(1000))) - assertEquals("-2.3K%", formatPerpsSignedPercent(BigDecimal(-2300))) - assertEquals("+500%", formatPerpsSignedPercent(BigDecimal(500))) - assertEquals("+999.99%", formatPerpsSignedPercent(BigDecimal(999.99))) - assertEquals("+10.5K%", formatPerpsSignedPercent(BigDecimal(10500))) - } - - private fun market( - marketId: String, - change: String, - ) = PerpsMarket( - marketId = marketId, - displaySymbol = marketId, - tokenSymbol = marketId, - quoteSymbol = "USDT", - markPrice = "1", - priceScale = 2, - leverage = 10, - iconUrl = "", - category = "", - tags = emptyList(), - fundingRate = "0", - minAmount = "0", - maxAmount = "0", - last = "1", - volume = "0", - high = "1", - low = "1", - open = "1", - change = change, - bidPrice = "1", - askPrice = "1", - createdAt = "", - updatedAt = "", - ) -} From ad3f51419f9908abc3895194d2c3ea6ed2e80a4f Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 27 May 2026 20:00:05 +0800 Subject: [PATCH 18/27] fix(perps): adjust top movers leverage label height --- .../web3/trade/perps/ClosedActivityItem.kt | 2 +- .../home/web3/trade/perps/OpenPositionItem.kt | 6 +- .../home/web3/trade/perps/OpenedOrderItem.kt | 2 +- .../home/web3/trade/perps/PerpsMarketItem.kt | 2 +- .../ui/home/web3/trade/perps/TopMoversCard.kt | 4 +- .../main/res/drawable/market_logo_dark.xml | 142 +++++++++++++----- .../main/res/drawable/market_logo_light.xml | 142 +++++++++++++----- app/src/main/res/drawable/perpetual_logo.xml | 130 ++++++++++++---- .../layout/fragment_market_share_bottom.xml | 7 +- 9 files changed, 319 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt index b00244e567..8474737cd9 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt @@ -116,7 +116,7 @@ fun ClosedActivityItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(leverageBackgroundColor) - .padding(horizontal = 3.dp, vertical = 2.dp), + .padding(horizontal = 3.dp, vertical = 1.dp), ) } Spacer(modifier = Modifier.height(4.dp)) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt index db761d6047..236be85224 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt @@ -133,7 +133,7 @@ fun OpenPositionItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(leverageBackgroundColor) - .padding(horizontal = 3.dp, vertical = 2.dp) + .padding(horizontal = 3.dp, vertical = 1.dp) ) if (tpSlTagText != null) { Spacer(modifier = Modifier.width(4.dp)) @@ -227,13 +227,13 @@ private fun TpSlStatusTag( val backgroundColor = Color(LocalContext.current.colorAttr(R.attr.bg_market_gradient_start)) Text( text = text, - fontSize = 11.sp, + fontSize = 12.sp, fontWeight = FontWeight.W500, color = Color.White, lineHeight = 14.sp, modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(backgroundColor) - .padding(horizontal = 4.dp, vertical = 2.dp), + .padding(horizontal = 3.dp, vertical = 1.dp), ) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt index d7cdbfc45a..2073aa471d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt @@ -124,7 +124,7 @@ fun OpenedOrderItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(leverageBackgroundColor) - .padding(horizontal = 3.dp, vertical = 2.dp), + .padding(horizontal = 3.dp, vertical = 1.dp), ) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt index ae55dc5b02..bd16887eb3 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt @@ -97,7 +97,7 @@ fun PerpsMarketItem( modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(MixinAppTheme.colors.backgroundGrayLight) - .padding(horizontal = 3.dp, vertical = 2.dp) + .padding(horizontal = 3.dp, vertical = 1.dp) ) } Text( diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index d4c040d4af..43e18fcfb9 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -180,14 +180,14 @@ private fun TopMoverGridItem( BasicText( text = changeText, style = TextStyle( - fontSize = 14.sp, + fontSize = 13.sp, color = changeColor, textAlign = TextAlign.Center, ), maxLines = 1, autoSize = TextAutoSize.StepBased( minFontSize = 8.sp, - maxFontSize = 14.sp, + maxFontSize = 13.sp, stepSize = 0.5.sp ), ) diff --git a/app/src/main/res/drawable/market_logo_dark.xml b/app/src/main/res/drawable/market_logo_dark.xml index b506c2d967..e3834a3a11 100644 --- a/app/src/main/res/drawable/market_logo_dark.xml +++ b/app/src/main/res/drawable/market_logo_dark.xml @@ -1,35 +1,18 @@ - + android:width="86dp" + android:height="20dp" + android:viewportWidth="86" + android:viewportHeight="20"> + android:pathData="M22.671,1C22.85,1 22.994,1.145 23,1.325L23,1.335V18.665C23,18.715 22.989,18.764 22.968,18.809C22.892,18.973 22.702,19.045 22.54,18.972L22.53,18.968L18.318,16.925C18.121,16.83 17.995,16.63 17.99,16.409L17.989,16.395V3.31C17.989,3.076 18.126,2.865 18.336,2.772L18.348,2.766L22.548,1.025C22.587,1.008 22.629,1 22.671,1ZM0.329,1C0.371,1 0.413,1.008 0.452,1.025L4.652,2.766C4.869,2.856 5.011,3.071 5.011,3.31V16.395C5.011,16.621 4.883,16.828 4.682,16.925L0.47,18.968C0.306,19.047 0.11,18.976 0.032,18.809C0.011,18.764 0,18.715 0,18.665V1.335C0,1.15 0.147,1 0.329,1ZM11.87,4.785L15.754,7.074C15.983,7.209 16.124,7.458 16.124,7.728V12.305C16.124,12.575 15.983,12.824 15.754,12.959L11.87,15.248C11.641,15.382 11.359,15.382 11.131,15.248L7.246,12.959C7.017,12.824 6.876,12.575 6.876,12.305V7.728C6.876,7.458 7.017,7.209 7.246,7.074L11.131,4.785C11.359,4.651 11.641,4.651 11.87,4.785Z" + android:fillType="evenOdd"> @@ -37,18 +20,103 @@ + android:pathData="M58.451,5.807V9.999H56.781V2.845H58.377V4.061H58.46C58.623,3.66 58.883,3.342 59.24,3.106C59.6,2.87 60.044,2.752 60.573,2.752C61.062,2.752 61.488,2.858 61.851,3.069C62.217,3.28 62.5,3.586 62.7,3.986C62.903,4.387 63.003,4.873 63,5.444V9.999H61.33V5.705C61.33,5.227 61.207,4.853 60.961,4.582C60.718,4.312 60.381,4.177 59.95,4.177C59.658,4.177 59.398,4.242 59.171,4.373C58.946,4.5 58.77,4.685 58.64,4.927C58.514,5.169 58.451,5.463 58.451,5.807Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + + + + + + + + + + + + + + android:pathData="M50.847,18.583C50.447,18.583 50.103,18.494 49.814,18.317C49.525,18.14 49.303,17.896 49.147,17.585C48.992,17.274 48.914,16.919 48.914,16.52C48.914,16.114 48.994,15.755 49.153,15.445C49.314,15.132 49.538,14.888 49.825,14.713C50.114,14.536 50.451,14.447 50.836,14.447C51.136,14.447 51.406,14.499 51.646,14.604C51.887,14.708 52.084,14.854 52.238,15.041C52.392,15.229 52.487,15.447 52.524,15.697H51.869C51.819,15.515 51.708,15.354 51.535,15.213C51.365,15.071 51.136,14.999 50.847,14.999C50.591,14.999 50.367,15.062 50.175,15.187C49.984,15.31 49.835,15.485 49.728,15.71C49.622,15.934 49.569,16.197 49.569,16.499C49.569,16.808 49.621,17.077 49.725,17.307C49.83,17.536 49.979,17.714 50.169,17.84C50.362,17.967 50.588,18.031 50.847,18.031C51.017,18.031 51.172,18.003 51.311,17.947C51.449,17.892 51.567,17.812 51.663,17.708C51.759,17.604 51.828,17.479 51.869,17.333H52.524C52.487,17.569 52.395,17.781 52.249,17.971C52.105,18.158 51.913,18.307 51.674,18.419C51.437,18.528 51.161,18.583 50.847,18.583Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + android:pathData="M45.994,18.593C45.724,18.593 45.479,18.545 45.258,18.45C45.038,18.353 44.863,18.213 44.733,18.031C44.604,17.847 44.539,17.624 44.539,17.364C44.539,17.135 44.587,16.949 44.683,16.807C44.78,16.663 44.908,16.55 45.069,16.468C45.23,16.386 45.408,16.326 45.603,16.286C45.799,16.244 45.996,16.211 46.194,16.187C46.453,16.156 46.663,16.132 46.825,16.117C46.987,16.099 47.106,16.07 47.18,16.031C47.256,15.991 47.294,15.921 47.294,15.822V15.801C47.294,15.545 47.219,15.345 47.069,15.203C46.921,15.06 46.696,14.989 46.394,14.989C46.081,14.989 45.836,15.053 45.658,15.182C45.48,15.31 45.355,15.447 45.283,15.593L44.661,15.385C44.772,15.142 44.92,14.953 45.106,14.817C45.292,14.68 45.496,14.584 45.716,14.531C45.939,14.475 46.157,14.447 46.372,14.447C46.509,14.447 46.666,14.463 46.844,14.494C47.023,14.524 47.197,14.585 47.363,14.679C47.532,14.773 47.671,14.914 47.783,15.104C47.894,15.293 47.949,15.546 47.949,15.864V18.499H47.294V17.958H47.26C47.216,18.044 47.142,18.137 47.038,18.236C46.935,18.335 46.797,18.419 46.625,18.489C46.452,18.558 46.242,18.593 45.994,18.593ZM46.094,18.041C46.353,18.041 46.572,17.993 46.75,17.898C46.929,17.802 47.064,17.679 47.155,17.528C47.248,17.377 47.294,17.218 47.294,17.051V16.489C47.266,16.52 47.205,16.549 47.111,16.575C47.018,16.599 46.911,16.621 46.788,16.64C46.668,16.657 46.55,16.673 46.436,16.687C46.323,16.699 46.231,16.709 46.161,16.718C45.991,16.739 45.831,16.773 45.683,16.82C45.537,16.865 45.418,16.933 45.328,17.025C45.239,17.116 45.194,17.239 45.194,17.395C45.194,17.609 45.279,17.77 45.447,17.879C45.617,17.987 45.833,18.041 46.094,18.041Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + android:pathData="M44.034,14.5L42.457,18.5H41.79L40.213,14.5H40.924L42.101,17.687H42.146L43.323,14.5H44.034Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + android:pathData="M38.723,18.5V14.5H39.379V18.5H38.723ZM39.056,13.833C38.929,13.833 38.819,13.792 38.726,13.711C38.635,13.629 38.59,13.531 38.59,13.417C38.59,13.302 38.635,13.204 38.726,13.122C38.819,13.041 38.929,13 39.056,13C39.184,13 39.293,13.041 39.384,13.122C39.477,13.204 39.523,13.302 39.523,13.417C39.523,13.531 39.477,13.629 39.384,13.711C39.293,13.792 39.184,13.833 39.056,13.833Z" + android:strokeAlpha="0.9" + android:fillColor="#D3D4D5" + android:fillAlpha="0.9"/> + + diff --git a/app/src/main/res/drawable/market_logo_light.xml b/app/src/main/res/drawable/market_logo_light.xml index 0998c1b404..b201047a9b 100644 --- a/app/src/main/res/drawable/market_logo_light.xml +++ b/app/src/main/res/drawable/market_logo_light.xml @@ -1,35 +1,18 @@ - + android:width="86dp" + android:height="20dp" + android:viewportWidth="86" + android:viewportHeight="20"> + android:pathData="M22.671,1C22.85,1 22.994,1.145 23,1.325L23,1.335V18.665C23,18.715 22.989,18.764 22.968,18.809C22.892,18.973 22.702,19.045 22.54,18.972L22.53,18.968L18.318,16.925C18.121,16.83 17.995,16.63 17.99,16.409L17.989,16.395V3.31C17.989,3.076 18.126,2.865 18.336,2.772L18.348,2.766L22.548,1.025C22.587,1.008 22.629,1 22.671,1ZM0.329,1C0.371,1 0.413,1.008 0.452,1.025L4.652,2.766C4.869,2.856 5.011,3.071 5.011,3.31V16.395C5.011,16.621 4.883,16.828 4.682,16.925L0.47,18.968C0.306,19.047 0.11,18.976 0.032,18.809C0.011,18.764 0,18.715 0,18.665V1.335C0,1.15 0.147,1 0.329,1ZM11.87,4.785L15.754,7.074C15.983,7.209 16.124,7.458 16.124,7.728V12.305C16.124,12.575 15.983,12.824 15.754,12.959L11.87,15.248C11.641,15.382 11.359,15.382 11.131,15.248L7.246,12.959C7.017,12.824 6.876,12.575 6.876,12.305V7.728C6.876,7.458 7.017,7.209 7.246,7.074L11.131,4.785C11.359,4.651 11.641,4.651 11.87,4.785Z" + android:fillType="evenOdd"> @@ -37,18 +20,103 @@ + android:pathData="M58.451,5.807V9.999H56.781V2.845H58.377V4.061H58.46C58.623,3.66 58.883,3.342 59.24,3.106C59.6,2.87 60.044,2.752 60.573,2.752C61.062,2.752 61.488,2.858 61.851,3.069C62.217,3.28 62.5,3.586 62.7,3.986C62.903,4.387 63.003,4.873 63,5.444V9.999H61.33V5.705C61.33,5.227 61.207,4.853 60.961,4.582C60.718,4.312 60.381,4.177 59.95,4.177C59.658,4.177 59.398,4.242 59.171,4.373C58.946,4.5 58.77,4.685 58.64,4.927C58.514,5.169 58.451,5.463 58.451,5.807Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + + + + + + + + + + + + + + android:pathData="M50.847,18.583C50.447,18.583 50.103,18.494 49.814,18.317C49.525,18.14 49.303,17.896 49.147,17.585C48.992,17.274 48.914,16.919 48.914,16.52C48.914,16.114 48.994,15.755 49.153,15.445C49.314,15.132 49.538,14.888 49.825,14.713C50.114,14.536 50.451,14.447 50.836,14.447C51.136,14.447 51.406,14.499 51.646,14.604C51.887,14.708 52.084,14.854 52.238,15.041C52.392,15.229 52.487,15.447 52.524,15.697H51.869C51.819,15.515 51.708,15.354 51.535,15.213C51.365,15.071 51.136,14.999 50.847,14.999C50.591,14.999 50.367,15.062 50.175,15.187C49.984,15.31 49.835,15.485 49.728,15.71C49.622,15.934 49.569,16.197 49.569,16.499C49.569,16.808 49.621,17.077 49.725,17.307C49.83,17.536 49.979,17.714 50.169,17.84C50.362,17.967 50.588,18.031 50.847,18.031C51.017,18.031 51.172,18.003 51.311,17.947C51.449,17.892 51.567,17.812 51.663,17.708C51.759,17.604 51.828,17.479 51.869,17.333H52.524C52.487,17.569 52.395,17.781 52.249,17.971C52.105,18.158 51.913,18.307 51.674,18.419C51.437,18.528 51.161,18.583 50.847,18.583Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + android:pathData="M45.994,18.593C45.724,18.593 45.479,18.545 45.258,18.45C45.038,18.353 44.863,18.213 44.733,18.031C44.604,17.847 44.539,17.624 44.539,17.364C44.539,17.135 44.587,16.949 44.683,16.807C44.78,16.663 44.908,16.55 45.069,16.468C45.23,16.386 45.408,16.326 45.603,16.286C45.799,16.244 45.996,16.211 46.194,16.187C46.453,16.156 46.663,16.132 46.825,16.117C46.987,16.099 47.106,16.07 47.18,16.031C47.256,15.991 47.294,15.921 47.294,15.822V15.801C47.294,15.545 47.219,15.345 47.069,15.203C46.921,15.06 46.696,14.989 46.394,14.989C46.081,14.989 45.836,15.053 45.658,15.182C45.48,15.31 45.355,15.447 45.283,15.593L44.661,15.385C44.772,15.142 44.92,14.953 45.106,14.817C45.292,14.68 45.496,14.584 45.716,14.531C45.939,14.475 46.157,14.447 46.372,14.447C46.509,14.447 46.666,14.463 46.844,14.494C47.023,14.524 47.197,14.585 47.363,14.679C47.532,14.773 47.671,14.914 47.783,15.104C47.894,15.293 47.949,15.546 47.949,15.864V18.499H47.294V17.958H47.26C47.216,18.044 47.142,18.137 47.038,18.236C46.935,18.335 46.797,18.419 46.625,18.489C46.452,18.558 46.242,18.593 45.994,18.593ZM46.094,18.041C46.353,18.041 46.572,17.993 46.75,17.898C46.929,17.802 47.064,17.679 47.155,17.528C47.248,17.377 47.294,17.218 47.294,17.051V16.489C47.266,16.52 47.205,16.549 47.111,16.575C47.018,16.599 46.911,16.621 46.788,16.64C46.668,16.657 46.55,16.673 46.436,16.687C46.323,16.699 46.231,16.709 46.161,16.718C45.991,16.739 45.831,16.773 45.683,16.82C45.537,16.865 45.418,16.933 45.328,17.025C45.239,17.116 45.194,17.239 45.194,17.395C45.194,17.609 45.279,17.77 45.447,17.879C45.617,17.987 45.833,18.041 46.094,18.041Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + android:pathData="M44.034,14.5L42.457,18.5H41.79L40.213,14.5H40.924L42.101,17.687H42.146L43.323,14.5H44.034Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + android:pathData="M38.723,18.5V14.5H39.379V18.5H38.723ZM39.056,13.833C38.929,13.833 38.819,13.792 38.726,13.711C38.635,13.629 38.59,13.531 38.59,13.417C38.59,13.302 38.635,13.204 38.726,13.122C38.819,13.041 38.929,13 39.056,13C39.184,13 39.293,13.041 39.384,13.122C39.477,13.204 39.523,13.302 39.523,13.417C39.523,13.531 39.477,13.629 39.384,13.711C39.293,13.792 39.184,13.833 39.056,13.833Z" + android:strokeAlpha="0.9" + android:fillColor="#333333" + android:fillAlpha="0.9"/> + + diff --git a/app/src/main/res/drawable/perpetual_logo.xml b/app/src/main/res/drawable/perpetual_logo.xml index 818129b86a..8a4f128c27 100644 --- a/app/src/main/res/drawable/perpetual_logo.xml +++ b/app/src/main/res/drawable/perpetual_logo.xml @@ -1,46 +1,112 @@ - + android:width="86dp" + android:height="20dp" + android:viewportWidth="86" + android:viewportHeight="20"> + android:fillType="evenOdd" + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + android:fillAlpha="0.9"/> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_market_share_bottom.xml b/app/src/main/res/layout/fragment_market_share_bottom.xml index 2a86e7b235..904818681f 100644 --- a/app/src/main/res/layout/fragment_market_share_bottom.xml +++ b/app/src/main/res/layout/fragment_market_share_bottom.xml @@ -65,14 +65,13 @@ android:layout_width="match_parent" android:layout_height="42dp" android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingStart="20dp" - android:paddingEnd="20dp"> + android:orientation="horizontal"> From da7265e722ecbd1bd106f5369552d0664008a071 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 10:30:28 +0800 Subject: [PATCH 19/27] fix(perps): add debounce to open/increase position buttons Prevent multiple BottomSheet dialogs from opening when users tap the open position or add position buttons rapidly. --- .../home/web3/trade/perps/OpenPositionPage.kt | 21 ++++++++++++------- .../web3/trade/perps/PerpsMarketDetailPage.kt | 9 ++++++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt index 31c2acc53b..df3c5111ac 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt @@ -145,6 +145,7 @@ fun OpenPositionPage( var takeProfitPrice by remember { mutableStateOf("") } var stopLossPrice by remember { mutableStateOf("") } var errorInfo by remember { mutableStateOf(null) } + var isProcessing by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val savedLeverage = remember(marketId) { @@ -672,35 +673,40 @@ fun OpenPositionPage( .fillMaxWidth() .height(48.dp), onClick = { + if (isProcessing) return@MixinButton + isProcessing = true AnalyticsTracker.trackPerpsPreview(leverage.toInt().toPerpsLeverageValue()) errorInfo = null - val token = currentToken ?: return@MixinButton - val amount = usdtAmount.toBigDecimalOrNull() ?: return@MixinButton + val token = currentToken ?: run { isProcessing = false; return@MixinButton } + val amount = usdtAmount.toBigDecimalOrNull() ?: run { isProcessing = false; return@MixinButton } - if (amount <= BigDecimal.ZERO) return@MixinButton + if (amount <= BigDecimal.ZERO) { isProcessing = false; return@MixinButton } if (minimumMargin > BigDecimal.ZERO && amount < minimumMargin) { errorInfo = minimumMarginError + isProcessing = false return@MixinButton } if (maximumMargin > BigDecimal.ZERO && amount > maximumMargin) { errorInfo = maximumMarginError + isProcessing = false return@MixinButton } - if (amount > (token.balance.toBigDecimalOrNull() ?: BigDecimal.ZERO)) return@MixinButton + if (amount > (token.balance.toBigDecimalOrNull() ?: BigDecimal.ZERO)) { isProcessing = false; return@MixinButton } val m = currentMarket val walletId = Session.getAccountId() ?: "" // Privacy Wallet - if (walletId.isEmpty()) return@MixinButton + if (walletId.isEmpty()) { isProcessing = false; return@MixinButton } - val activity = context as? FragmentActivity ?: return@MixinButton + val activity = context as? FragmentActivity ?: run { isProcessing = false; return@MixinButton } val price = m.last.toBigDecimalOrNull() ?: BigDecimal.ZERO - if (price == BigDecimal.ZERO) return@MixinButton + if (price == BigDecimal.ZERO) { isProcessing = false; return@MixinButton } scope.launch { val hasOpeningPosition = viewModel.getOpenPositionsFromDb(walletId) .any { it.marketId == m.marketId } if (hasOpeningPosition) { errorInfo = waitingOtherOrdersError + isProcessing = false return@launch } @@ -732,6 +738,7 @@ fun OpenPositionPage( }.show(activity.supportFragmentManager, PerpsConfirmBottomSheetDialogFragment.TAG) }, onError = { errorCode, errorMessage -> + isProcessing = false errorInfo = if (errorCode > 0) { context.getMixinErrorStringByCode(errorCode, errorMessage) } else { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt index 3a4eb0ddac..3d8b6582ce 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt @@ -116,6 +116,7 @@ fun PerpsMarketDetailPage( } }.collectAsStateWithLifecycle(initialValue = emptyList()) var previousOpenPositionsCount by remember(walletId) { mutableStateOf(null) } + var isAddingProcessing by remember { mutableStateOf(false) } val currentPosition = openPositions?.firstOrNull { it.marketId == marketId } val hasLoadedOpenPositions = openPositions != null val closedPositions = allClosedPositions.filter { it.marketId == marketId } @@ -465,12 +466,15 @@ fun PerpsMarketDetailPage( modifier = Modifier .weight(1f) .height(48.dp), - enabled = true, + enabled = !isAddingProcessing, onClick = { - val activity = context as? FragmentActivity ?: return@MixinButton + if (isAddingProcessing) return@MixinButton + isAddingProcessing = true + val activity = context as? FragmentActivity ?: run { isAddingProcessing = false; return@MixinButton } val positionForAdd = currentPosition PerpsAddBottomSheetDialogFragment.newInstance(positionForAdd) .setOnAdd { token, amount -> + isAddingProcessing = false val referencePrice = market?.last ?: positionForAdd.markPrice ?: positionForAdd.entryPrice @@ -503,6 +507,7 @@ fun PerpsMarketDetailPage( ).show(activity.supportFragmentManager, PerpsConfirmBottomSheetDialogFragment.TAG) }, onError = { errorCode, errorMessage -> + isAddingProcessing = false val message = if (errorCode > 0) { context.getMixinErrorStringByCode(errorCode, errorMessage) } else { From a7a996e85ba5a09e89fc82947cb38ef08672807e Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 11:18:33 +0800 Subject: [PATCH 20/27] fix(perps): use opening state for pending positions --- .../api/response/perps/PerpsPosition.kt | 8 +++++++- .../android/db/perps/PerpsPositionDao.kt | 18 ++++++++--------- .../home/web3/trade/perps/OpenPositionItem.kt | 5 +++-- .../web3/trade/perps/PerpetualViewModel.kt | 20 +++++++++++++++---- .../web3/trade/perps/PerpsMarketDetailPage.kt | 7 ++++--- .../web3/trade/perps/PositionDetailPage.kt | 6 ++++-- 6 files changed, 43 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt index d11b68667f..398f4a96df 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsPosition.kt @@ -74,4 +74,10 @@ data class PerpsPosition( @SerializedName("updated_at") @ColumnInfo(name = "updated_at") val updatedAt: String, -) : Parcelable +) : Parcelable { + companion object { + const val STATE_OPEN = "open" + const val STATE_OPENING = "opening" + const val STATE_ADDING = "adding" + } +} diff --git a/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt b/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt index 0d2f20d5ea..0ef4439edf 100644 --- a/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt +++ b/app/src/main/java/one/mixin/android/db/perps/PerpsPositionDao.kt @@ -22,7 +22,7 @@ interface PerpsPositionDao : BaseDao { SELECT p.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM positions p LEFT JOIN markets m ON m.market_id = p.market_id - WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'processing' or p.state = 'adding') + WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'opening' or p.state = 'adding') ORDER BY p.created_at DESC """) suspend fun getOpenPositions(walletId: String): List @@ -32,7 +32,7 @@ interface PerpsPositionDao : BaseDao { SELECT p.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM positions p LEFT JOIN markets m ON m.market_id = p.market_id - WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'processing' or p.state = 'adding') + WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'opening' or p.state = 'adding') ORDER BY p.created_at DESC """ ) @@ -42,7 +42,7 @@ interface PerpsPositionDao : BaseDao { SELECT p.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM positions p LEFT JOIN markets m ON m.market_id = p.market_id - WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'processing' or p.state = 'adding') + WHERE p.wallet_id = :walletId AND (p.state = 'open' or p.state = 'opening' or p.state = 'adding') ORDER BY p.created_at DESC """) fun getOpenPositionsPaged(walletId: String): PagingSource @@ -69,22 +69,22 @@ interface PerpsPositionDao : BaseDao { @Query("DELETE FROM positions WHERE wallet_id = :walletId") suspend fun deleteByWallet(walletId: String) - @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") suspend fun deleteOpenByWallet(walletId: String) - @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding') AND position_id NOT IN (:positionIds)") + @Query("DELETE FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding') AND position_id NOT IN (:positionIds)") suspend fun deleteOpenByWalletAndNotIn(walletId: String, positionIds: List) - @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' OR state = 'processing' OR state = 'adding')") + @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' OR state = 'opening' OR state = 'adding')") suspend fun getTotalUnrealizedPnl(walletId: String): Double? - @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("SELECT COALESCE(SUM(CAST(unrealized_pnl AS REAL)), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") fun observeTotalUnrealizedPnl(walletId: String): Flow - @Query("SELECT SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("SELECT SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") suspend fun getTotalOpenPositionValue(walletId: String): Double? - @Query("SELECT COALESCE(SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'processing' or state = 'adding')") + @Query("SELECT COALESCE(SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))), 0) FROM positions WHERE wallet_id = :walletId AND (state = 'open' or state = 'opening' or state = 'adding')") fun observeTotalOpenPositionValue(walletId: String): Flow @Query("DELETE FROM positions WHERE position_id = :positionId") diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt index 236be85224..b5dd8782d4 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.Constants import one.mixin.android.R +import one.mixin.android.api.response.perps.PerpsPosition import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme @@ -56,8 +57,8 @@ fun OpenPositionItem( ?.toPlainString() ?: position.quantity.removePrefix("-") val isLong = position.side.equals("long", true) - val isOpening = position.state.equals("processing", true) - val isAdding = position.state.equals("adding", true) + val isOpening = position.state.equals(PerpsPosition.STATE_OPENING, true) + val isAdding = position.state.equals(PerpsPosition.STATE_ADDING, true) val isPending = isOpening || isAdding val sideColor = if (isLong) { if (quoteColorPref) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt index 48ee455ba2..3e554d085a 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpetualViewModel.kt @@ -343,11 +343,23 @@ class PerpetualViewModel @Inject constructor( if (response.isSuccess && data != null) { Timber.d("Perps order opened: ${data.orderId}, payUrl: ${data.paymentUrl}") + val entryPriceDecimal = entryPrice.toBigDecimalOrNull() ?: BigDecimal.ZERO + val amountDecimal = amount.toBigDecimalOrNull() ?: BigDecimal.ZERO + val quantityValue = if (entryPriceDecimal > BigDecimal.ZERO) { + amountDecimal + .multiply(BigDecimal(leverage)) + .divide(entryPriceDecimal, 8, java.math.RoundingMode.HALF_UP) + .stripTrailingZeros() + .toPlainString() + } else { + "0" + } + val position = PerpsPosition( positionId = data.orderId, marketId = marketId, side = side, - quantity = amount, + quantity = quantityValue, settleAssetId = assetId, botId = "", entryPrice = entryPrice, @@ -358,7 +370,7 @@ class PerpetualViewModel @Inject constructor( stopLossPrice = stopLossPrice, liquidationPrice = null, leverage = leverage, - state = "processing", + state = PerpsPosition.STATE_OPENING, markPrice = entryPrice, unrealizedPnl = "0", roe = "0", @@ -425,12 +437,12 @@ class PerpetualViewModel @Inject constructor( if (localPosition != null) { perpsPositionDao.upsertSuspend( localPosition.copy( - state = "adding", + state = PerpsPosition.STATE_ADDING, updatedAt = now, ) ) } else { - perpsPositionDao.updateStatus(positionId, "adding", now) + perpsPositionDao.updateStatus(positionId, PerpsPosition.STATE_ADDING, now) } } onSuccess(data) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt index 3d8b6582ce..4a6861d0cf 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt @@ -61,6 +61,7 @@ import one.mixin.android.R import one.mixin.android.api.response.perps.PerpsMarket import one.mixin.android.api.response.perps.PerpsOrder import one.mixin.android.api.response.perps.PerpsOrderItem +import one.mixin.android.api.response.perps.PerpsPosition import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.api.response.perps.toPosition import one.mixin.android.compose.CoilImage @@ -437,9 +438,9 @@ fun PerpsMarketDetailPage( .padding(bottom = 20.dp, top = 20.dp) ) { if (currentPosition != null) { - val isOpen = currentPosition.state == "open" - val isAdding = currentPosition.state == "adding" - val isPending = currentPosition.state == "processing" || isAdding + val isOpen = currentPosition.state == PerpsPosition.STATE_OPEN + val isAdding = currentPosition.state == PerpsPosition.STATE_ADDING + val isPending = currentPosition.state == PerpsPosition.STATE_OPENING || isAdding if (isPending) { MixinButton( modifier = Modifier diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt index f573c827df..c7971e5187 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt @@ -44,6 +44,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import one.mixin.android.R import one.mixin.android.api.response.perps.PerpsOrder import one.mixin.android.api.response.perps.PerpsOrderItem +import one.mixin.android.api.response.perps.PerpsPosition import one.mixin.android.api.response.perps.PerpsPositionItem import one.mixin.android.compose.CoilImage import one.mixin.android.compose.theme.MixinAppTheme @@ -137,7 +138,8 @@ fun PositionDetailPage( } } - val isPending = position.state == "processing" || position.state == "adding" + val isAdding = position.state == PerpsPosition.STATE_ADDING + val isPending = position.state == PerpsPosition.STATE_OPENING || isAdding val leverageTextColor = if (isPending) MixinAppTheme.colors.textAssist else sideColor val leverageBackgroundColor = if (isPending) { MixinAppTheme.colors.backgroundGrayLight @@ -296,7 +298,7 @@ fun PositionDetailPage( ) { if (isPending) { Text( - text = stringResource(if (position.state == "adding") R.string.adding_position else R.string.Pending), + text = stringResource(if (isAdding) R.string.adding_position else R.string.Pending), color = MixinAppTheme.colors.textAssist, fontWeight = FontWeight.W500, modifier = Modifier From bbe03158715d649dd3cfc31f922787819c3e3947 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 15:29:39 +0800 Subject: [PATCH 21/27] fix(perps): format position detail prices by scale --- .../api/response/perps/PerpsOrderItem.kt | 2 ++ .../mixin/android/db/perps/PerpsOrderDao.kt | 12 +++++----- .../web3/trade/perps/PositionDetailPage.kt | 23 ++++--------------- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt index 1a0ff3b534..ea4364c79a 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt @@ -63,4 +63,6 @@ data class PerpsOrderItem( val iconUrl: String? = null, @ColumnInfo(name = "token_symbol") val tokenSymbol: String? = null, + @ColumnInfo(name = "price_scale") + val priceScale: Int = 2, ) : Parcelable diff --git a/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt b/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt index ed6e5b5508..cea57b84ff 100644 --- a/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt +++ b/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt @@ -19,7 +19,7 @@ interface PerpsOrderDao : BaseDao { suspend fun insertAll(orders: List) @Query(""" - SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol + SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM perps_orders o LEFT JOIN markets m ON m.market_id = o.market_id WHERE o.order_type IN ('open', 'increase_position', 'close') @@ -31,7 +31,7 @@ interface PerpsOrderDao : BaseDao { suspend fun getOrders(limit: Int, offset: String? = null): List @Query(""" - SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol + SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM perps_orders o LEFT JOIN markets m ON m.market_id = o.market_id WHERE o.order_type IN ('open', 'increase_position', 'close') @@ -42,7 +42,7 @@ interface PerpsOrderDao : BaseDao { fun observeOrders(limit: Int): Flow> @Query(""" - SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol + SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM perps_orders o LEFT JOIN markets m ON m.market_id = o.market_id WHERE o.order_type IN ('open', 'increase_position', 'close') @@ -52,7 +52,7 @@ interface PerpsOrderDao : BaseDao { fun getOrdersPaged(): PagingSource @Query(""" - SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol + SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM perps_orders o LEFT JOIN markets m ON m.market_id = o.market_id WHERE o.order_type IN ('open', 'increase_position', 'close') @@ -64,7 +64,7 @@ interface PerpsOrderDao : BaseDao { suspend fun getOrdersByMarket(marketId: String, limit: Int = 100): List @Query(""" - SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol + SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM perps_orders o LEFT JOIN markets m ON m.market_id = o.market_id WHERE o.order_id = :orderId @@ -72,7 +72,7 @@ interface PerpsOrderDao : BaseDao { suspend fun getOrder(orderId: String): PerpsOrderItem? @Query(""" - SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol + SELECT o.*, m.display_symbol, m.icon_url, m.token_symbol, m.price_scale FROM perps_orders o LEFT JOIN markets m ON m.market_id = o.market_id WHERE o.position_id = :positionId diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt index c7971e5187..a00b38afeb 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt @@ -200,10 +200,6 @@ fun PositionDetailPage( return formatPerpsSignedRawUsdDecimal(value) } - fun formatPriceUsd(value: BigDecimal): String { - return formatPerpsUsdDecimal(value) - } - PageScaffold( title = title, verticalScrollable = false, @@ -397,7 +393,7 @@ fun PositionDetailPage( PositionDetailItem( label = stringResource(R.string.Entry_Price).uppercase(), - value = formatPriceUsd(entryPrice) + value = formatPerpsPrice(entryPrice, position.priceScale) ) Spacer(modifier = Modifier.height(20.dp)) @@ -551,10 +547,6 @@ fun PositionDetailPage( return formatPerpsSignedRawUsdDecimal(value) } - fun formatPriceUsd(value: BigDecimal): String { - return formatPerpsUsdDecimal(value) - } - PageScaffold( title = title, verticalScrollable = false, @@ -712,14 +704,14 @@ fun PositionDetailPage( PositionDetailItem( label = stringResource(R.string.Entry_Price).uppercase(), - value = formatPriceUsd(closeOrder.entryPrice.toBigDecimalOrNull() ?: BigDecimal.ZERO) + value = formatPerpsPrice(closeOrder.entryPrice, closeOrder.priceScale) ) Spacer(modifier = Modifier.height(20.dp)) PositionDetailItem( label = stringResource(R.string.Close_Price).uppercase(), - value = formatPriceUsd(closeOrder.closePrice.toBigDecimalOrNull() ?: BigDecimal.ZERO) + value = formatPerpsPrice(closeOrder.closePrice, closeOrder.priceScale) ) Spacer(modifier = Modifier.height(20.dp)) @@ -801,13 +793,8 @@ fun OpenedOrderDetailPage( val quantity = openedOrder.quantity.toBigDecimalOrNull() ?: BigDecimal.ZERO val absQuantity = quantity.abs() - val entryPrice = openedOrder.entryPrice.toBigDecimalOrNull() ?: BigDecimal.ZERO val leverage = openedOrder.leverage - fun formatPriceUsd(value: BigDecimal): String { - return formatPerpsUsdDecimal(value) - } - PageScaffold( title = title, verticalScrollable = false, @@ -942,13 +929,13 @@ fun OpenedOrderDetailPage( if (!isFailed) { PositionDetailItem( label = stringResource(R.string.Entry_Price).uppercase(), - value = formatPriceUsd(entryPrice) + value = formatPerpsPrice(openedOrder.entryPrice, openedOrder.priceScale) ) Spacer(modifier = Modifier.height(20.dp)) val amountValue = if (leverage > 0) { - absQuantity.multiply(entryPrice) + absQuantity.multiply(openedOrder.entryPrice.toBigDecimalOrNull() ?: BigDecimal.ZERO) .divide(BigDecimal(leverage), 8, RoundingMode.HALF_UP) } else { BigDecimal.ZERO From 6f19280c8c6be4e91c6a56bf3f6588af50019ee0 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 16:01:56 +0800 Subject: [PATCH 22/27] fix(perps): remove inner borders between connected open positions Draw only the group's outer border per item so adjacent open positions no longer show a divider line at their junctions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../home/web3/trade/perps/AllPositionsPage.kt | 69 +++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt index ec6d160048..9c2d43e913 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/AllPositionsPage.kt @@ -29,13 +29,20 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle @@ -315,20 +322,23 @@ private fun LazyListScope.openPositionItems( contentType = positions.itemContentType { "open_position" }, ) { index -> val position = positions[index] ?: return@items + val isFirst = index == 0 + val isLast = index == positions.itemCount - 1 val shape = when { - positions.itemCount == 1 -> RoundedCornerShape(8.dp) - index == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) - index == positions.itemCount - 1 -> RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp) + isFirst && isLast -> RoundedCornerShape(8.dp) + isFirst -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + isLast -> RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp) else -> RoundedCornerShape(0.dp) } Box( modifier = Modifier .fillMaxWidth() .clip(shape) - .cardBackground( + .groupedItemBorder( backgroundColor = MixinAppTheme.colors.background, borderColor = MixinAppTheme.colors.borderColor, - cornerRadius = if (index == 0 || index == positions.itemCount - 1) 8.dp else 0.dp, + isFirst = isFirst, + isLast = isLast, ) ) { OpenPositionItem( @@ -339,6 +349,55 @@ private fun LazyListScope.openPositionItems( } } +private fun Modifier.groupedItemBorder( + backgroundColor: Color, + borderColor: Color, + isFirst: Boolean, + isLast: Boolean, + cornerRadius: Dp = 8.dp, + borderWidth: Dp = 0.8.dp, +): Modifier = this.drawBehind { + drawRect(color = backgroundColor) + val r = cornerRadius.toPx() + val sw = borderWidth.toPx() + val half = sw / 2f + val w = size.width + val h = size.height + val left = half + val top = half + val right = w - half + val bottom = h - half + val path = Path() + when { + isFirst && isLast -> path.addRoundRect( + RoundRect(Rect(left, top, right, bottom), CornerRadius(r, r)), + ) + isFirst -> { + path.moveTo(left, h) + path.lineTo(left, top + r) + path.arcTo(Rect(left, top, left + 2 * r, top + 2 * r), 180f, 90f, false) + path.lineTo(right - r, top) + path.arcTo(Rect(right - 2 * r, top, right, top + 2 * r), 270f, 90f, false) + path.lineTo(right, h) + } + isLast -> { + path.moveTo(left, 0f) + path.lineTo(left, bottom - r) + path.arcTo(Rect(left, bottom - 2 * r, left + 2 * r, bottom), 180f, -90f, false) + path.lineTo(right - r, bottom) + path.arcTo(Rect(right - 2 * r, bottom - 2 * r, right, bottom), 90f, -90f, false) + path.lineTo(right, 0f) + } + else -> { + path.moveTo(left, 0f) + path.lineTo(left, h) + path.moveTo(right, 0f) + path.lineTo(right, h) + } + } + drawPath(path, color = borderColor, style = Stroke(width = sw)) +} + private fun LazyListScope.closedPositionItems( positions: LazyPagingItems, onPositionClick: (PerpsOrderItem) -> Unit, From a3019332fcac8832c7c5355ea716bb829868b4d3 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 16:32:33 +0800 Subject: [PATCH 23/27] style(perps): use medium weight for leverage labels Co-Authored-By: Claude Opus 4.7 (1M context) --- .../one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt | 2 ++ .../android/ui/home/web3/trade/perps/ClosedActivityItem.kt | 2 ++ .../mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt | 1 + .../mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt | 2 ++ .../mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt | 2 ++ 5 files changed, 9 insertions(+) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt index dcc9234f0e..3cc2fa84d0 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -116,6 +117,7 @@ fun ClosedPositionItem( Text( text = "${order.leverage}x", fontSize = 12.sp, + fontWeight = FontWeight.W500, color = sideColor, lineHeight = 14.sp, maxLines = 1, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt index 8474737cd9..d990762053 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -110,6 +111,7 @@ fun ClosedActivityItem( Text( text = "${order.leverage}x", fontSize = 12.sp, + fontWeight = FontWeight.W500, color = leverageColor, lineHeight = 14.sp, maxLines = 1, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt index b5dd8782d4..65ace247f2 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt @@ -129,6 +129,7 @@ fun OpenPositionItem( Text( text = "${position.leverage}x", fontSize = 12.sp, + fontWeight = FontWeight.W500, color = leverageTextColor, lineHeight = 14.sp, modifier = Modifier diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt index 2073aa471d..e119e8dd6f 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -118,6 +119,7 @@ fun OpenedOrderItem( Text( text = "${order.leverage}x", fontSize = 12.sp, + fontWeight = FontWeight.W500, color = leverageColor, lineHeight = 14.sp, maxLines = 1, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt index bd16887eb3..4dc0c9ebeb 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.R @@ -92,6 +93,7 @@ fun PerpsMarketItem( Text( text = "${market.leverage}x", fontSize = 12.sp, + fontWeight = FontWeight.W500, color = MixinAppTheme.colors.textAssist, lineHeight = 14.sp, modifier = Modifier From 17b3ea8e0fe3e2988d28461a88b97265b27dfc03 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 16:34:07 +0800 Subject: [PATCH 24/27] style(market): adjust share card header spacing --- .../layout/fragment_market_share_bottom.xml | 4 +++- .../item_perps_position_share_poster.xml | 22 +++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/fragment_market_share_bottom.xml b/app/src/main/res/layout/fragment_market_share_bottom.xml index 904818681f..8f923b06db 100644 --- a/app/src/main/res/layout/fragment_market_share_bottom.xml +++ b/app/src/main/res/layout/fragment_market_share_bottom.xml @@ -62,8 +62,10 @@ diff --git a/app/src/main/res/layout/item_perps_position_share_poster.xml b/app/src/main/res/layout/item_perps_position_share_poster.xml index 39c82db4cd..2e28e5b4ea 100644 --- a/app/src/main/res/layout/item_perps_position_share_poster.xml +++ b/app/src/main/res/layout/item_perps_position_share_poster.xml @@ -17,28 +17,26 @@ + android:paddingTop="16dp"> + app:layout_constraintTop_toTopOf="@id/pnl_tv" /> From 02fb9be0185c323a666723f1bdfeafba65ea32ed Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 16:58:32 +0800 Subject: [PATCH 25/27] Revert "style(perps): use medium weight for leverage labels" --- .../one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt | 2 -- .../android/ui/home/web3/trade/perps/ClosedActivityItem.kt | 2 -- .../mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt | 1 - .../mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt | 2 -- .../mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt | 2 -- .../one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt | 1 - 6 files changed, 10 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt index 3cc2fa84d0..dcc9234f0e 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -117,7 +116,6 @@ fun ClosedPositionItem( Text( text = "${order.leverage}x", fontSize = 12.sp, - fontWeight = FontWeight.W500, color = sideColor, lineHeight = 14.sp, maxLines = 1, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt index d990762053..8474737cd9 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -111,7 +110,6 @@ fun ClosedActivityItem( Text( text = "${order.leverage}x", fontSize = 12.sp, - fontWeight = FontWeight.W500, color = leverageColor, lineHeight = 14.sp, maxLines = 1, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt index 65ace247f2..b5dd8782d4 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionItem.kt @@ -129,7 +129,6 @@ fun OpenPositionItem( Text( text = "${position.leverage}x", fontSize = 12.sp, - fontWeight = FontWeight.W500, color = leverageTextColor, lineHeight = 14.sp, modifier = Modifier diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt index e119e8dd6f..2073aa471d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenedOrderItem.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -119,7 +118,6 @@ fun OpenedOrderItem( Text( text = "${order.leverage}x", fontSize = 12.sp, - fontWeight = FontWeight.W500, color = leverageColor, lineHeight = 14.sp, maxLines = 1, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt index 4dc0c9ebeb..bd16887eb3 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketItem.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import one.mixin.android.R @@ -93,7 +92,6 @@ fun PerpsMarketItem( Text( text = "${market.leverage}x", fontSize = 12.sp, - fontWeight = FontWeight.W500, color = MixinAppTheme.colors.textAssist, lineHeight = 14.sp, modifier = Modifier diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt index 43e18fcfb9..a110591997 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/TopMoversCard.kt @@ -144,7 +144,6 @@ private fun TopMoverGridItem( fontSize = 12.sp, lineHeight = 14.sp, color = MixinAppTheme.colors.textAssist, - fontWeight = FontWeight.W500, ), maxLines = 1, autoSize = TextAutoSize.StepBased( From cd2e14d4eb45646dec8729ba87bd10b288be6857 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 28 May 2026 17:05:57 +0800 Subject: [PATCH 26/27] Replace icon --- app/src/main/res/layout/fragment_market_share_bottom.xml | 2 +- .../main/res/layout/fragment_perps_position_share_bottom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_market_share_bottom.xml b/app/src/main/res/layout/fragment_market_share_bottom.xml index 8f923b06db..53fa0f05ba 100644 --- a/app/src/main/res/layout/fragment_market_share_bottom.xml +++ b/app/src/main/res/layout/fragment_market_share_bottom.xml @@ -32,7 +32,7 @@ android:background="?android:attr/selectableItemBackground" android:contentDescription="@null" android:padding="14dp" - android:src="@drawable/ic_float_close" /> + android:src="@drawable/ic_circle_close" /> + android:src="@drawable/ic_circle_close" /> Date: Thu, 28 May 2026 23:12:33 +0400 Subject: [PATCH 27/27] fix adding and opening status --- .../android/ui/home/web3/trade/perps/OpenPositionPage.kt | 2 ++ .../web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt | 7 +++++++ .../ui/home/web3/trade/perps/PerpsMarketDetailPage.kt | 3 +++ 3 files changed, 12 insertions(+) diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt index df3c5111ac..80989048d5 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/OpenPositionPage.kt @@ -735,6 +735,8 @@ fun OpenPositionPage( payUrl = response.paymentUrl ).setOnDone { onOpenSuccess(m.marketId) + }.setOnDestroy { + isProcessing = false }.show(activity.supportFragmentManager, PerpsConfirmBottomSheetDialogFragment.TAG) }, onError = { errorCode, errorMessage -> diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt index 3c8b5e7855..1c1749e3c1 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt @@ -113,12 +113,18 @@ class PerpsAddBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragment( } private var onAddAction: ((TokenItem, String) -> Unit)? = null + private var onDestroyAction: (() -> Unit)? = null fun setOnAdd(callback: (TokenItem, String) -> Unit): PerpsAddBottomSheetDialogFragment { onAddAction = callback return this } + fun setOnDestroy(callback: () -> Unit): PerpsAddBottomSheetDialogFragment { + onDestroyAction = callback + return this + } + override fun getTheme() = R.style.AppTheme_Dialog @SuppressLint("RestrictedApi") @@ -223,6 +229,7 @@ class PerpsAddBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragment( override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) + onDestroyAction?.invoke() } override fun dismiss() { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt index 4a6861d0cf..b227837e53 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsMarketDetailPage.kt @@ -474,6 +474,9 @@ fun PerpsMarketDetailPage( val activity = context as? FragmentActivity ?: run { isAddingProcessing = false; return@MixinButton } val positionForAdd = currentPosition PerpsAddBottomSheetDialogFragment.newInstance(positionForAdd) + .setOnDestroy { + isAddingProcessing = false + } .setOnAdd { token, amount -> isAddingProcessing = false val referencePrice = market?.last