Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
96bc40f
feat(perps): add Top Movers card above Trending
SeniorZhai May 25, 2026
d51d6bf
feat(perps): support initial sort for market list and refine top move…
SeniorZhai May 26, 2026
d86bbde
test(perps): fix instrumentation test crashes and migrate to compose …
SeniorZhai May 26, 2026
6041bb6
chore: ignore screenshots directory
SeniorZhai May 26, 2026
e0a1a1a
test(perps): add dual-theme screenshot test with real market data and…
SeniorZhai May 26, 2026
bfe75ab
style(perps): refine leverage badge position and item corner radius
SeniorZhai May 26, 2026
a0c762d
Merge branch 'master' into feat/perps-top-movers
SeniorZhai May 26, 2026
30fa3cc
Merge remote-tracking branch 'origin/master' into feat/perps-top-movers
SeniorZhai May 26, 2026
423219d
fix(perps): show top and bottom movers
SeniorZhai May 27, 2026
3d83e57
feat(perps): update top movers card text styling
SeniorZhai May 27, 2026
b9a44b2
fix(perps): add i18n for top movers label
SeniorZhai May 27, 2026
49543a9
feat(perps): format large percent values with K suffix
SeniorZhai May 27, 2026
976f643
fix(build): resolve protobuf-javalite dependency conflict
SeniorZhai May 27, 2026
a7702bb
feat(perps): add autoSize to top movers item text
SeniorZhai May 27, 2026
7ba5a51
fix(perps): stabilize position list item keys
SeniorZhai May 27, 2026
d599476
fix(perps): improve positions list rendering
SeniorZhai May 27, 2026
6cb03a8
fix(perps): update share link type
SeniorZhai May 27, 2026
9b9244a
Merge remote-tracking branch 'origin/master' into feat/perps-top-movers
SeniorZhai May 27, 2026
8c242d7
fix(perps): add shadow to top mover leverage label
SeniorZhai May 27, 2026
0f4ab2b
chore(test): revert protobuf test changes
SeniorZhai May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions app/src/main/java/one/mixin/android/MixinApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -306,24 +309,32 @@ private fun LazyListScope.openPositionItems(
onPositionClick: (PerpsPositionItem) -> Unit,
) {
if (positions.itemCount == 0) return
item {
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
OpenPositionItem(
position = position,
onClick = { onPositionClick(position) },
)
}
OpenPositionItem(
position = position,
onClick = { onPositionClick(position) },
)
}
}
}
Expand All @@ -332,7 +343,13 @@ private fun LazyListScope.closedPositionItems(
positions: LazyPagingItems<PerpsOrderItem>,
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -102,6 +103,9 @@ fun PerpetualContent(
val openPositionsCount = openPositions.size
val openPositionsPreview = openPositions.take(3)
val marketsPreview = markets.take(3)
val topMoversPreview = remember(markets) {
markets.topMoversPreview()
}
val sourceOrder = remember(markets) {
markets.withIndex().associate { it.value.marketId to it.index }
}
Expand Down Expand Up @@ -308,6 +312,25 @@ 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,
onViewAllClick = { onShowAllMarkets(null, MarketSort.TWENTY_FOUR_HOURS_PERCENTAGE_DESCENDING) },
onMarketItemClick = onMarketItemClick,
)
}
}

Spacer(modifier = Modifier.height(16.dp))

Column(
Expand All @@ -324,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) {
Expand Down Expand Up @@ -373,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,
)
Expand All @@ -399,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,
)
Expand Down Expand Up @@ -638,7 +661,6 @@ private fun calculatePnlPercent(
.toDouble()
}


@Composable
private fun ViewAllAction(onClick: () -> Unit) {
Row(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,20 @@ 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"

fun newInstance(isLong: Boolean) = PerpsMarketListBottomSheetDialogFragment().withArgs {
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) }
}
}

Expand All @@ -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<PerpsMarket>()
private var currentQuery = ""
private var currentCategory = MarketCategory.ALL
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading