Skip to content

Commit

Permalink
Implement pagination on premium leaderboard (#306)
Browse files Browse the repository at this point in the history
* Add new `premiumSince` order by to PremiumLeaderboard;
* Store profiles returned by leaderboard endpoints in database;
* Migrate `donatedBtc`  to `donatedSats` in legends leaderboard;
* Implement pagination on premium leaderboard;
* Increase limit on legend leaderboard from 200 to 300;
  • Loading branch information
markocic authored Feb 5, 2025
1 parent 098c0d6 commit 314ae5c
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fun PrimalEvent?.parseAndMapAsLeaderboardLegendEntries(profiles: Map<String, Pro
displayName = profile.authorNameUiFriendly(),
internetIdentifier = profile.internetIdentifier,
premiumProfileDataUi = profile.primalPremiumInfo?.asPremiumProfileDataUi(),
donatedBtc = item.donatedBtc,
donatedSats = item.donatedSats.toULong(),
)
}
} ?: emptyList()
Expand All @@ -31,7 +31,7 @@ fun PrimalEvent?.parseAndMapAsOGLeaderboardEntries(profiles: Map<String, Profile
?.mapNotNull { item ->
profiles[item.pubkey]?.let { profile ->
OGLeaderboardEntry(
index = item.index,
index = item.index.toInt(),
userId = item.pubkey,
avatarCdnImage = profile.avatarCdnImage,
displayName = profile.authorNameUiFriendly(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class ContentLegendLeaderboardItem(
val pubkey: String,
@SerialName("donated_btc") val donatedBtc: Double,
@SerialName("donated_btc") val donatedSats: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable

@Serializable
data class ContentPremiumLeaderboardItem(
val index: Int,
val index: Double,
val pubkey: String,
@SerialName("premium_since") val premiumSince: Long? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ interface PremiumApi {

suspend fun getLegendLeaderboard(orderBy: LegendLeaderboardOrderBy, limit: Int = 1000): LegendLeaderboardResponse

suspend fun getPremiumLeaderboard(orderBy: PremiumLeaderboardOrderBy, limit: Int = 100): PremiumLeaderboardResponse
suspend fun getPremiumLeaderboard(
since: Long?,
until: Long?,
orderBy: PremiumLeaderboardOrderBy,
limit: Int = 100,
): PremiumLeaderboardResponse

suspend fun getRecoveryContactsList(userId: String): List<NostrEvent>
}
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,20 @@ class PremiumApiImpl @Inject constructor(
}

override suspend fun getPremiumLeaderboard(
since: Long?,
until: Long?,
orderBy: PremiumLeaderboardOrderBy,
limit: Int,
): PremiumLeaderboardResponse {
val queryResult = primalCacheApiClient.query(
message = PrimalCacheFilter(
primalVerb = PrimalVerb.MEMBERSHIP_PREMIUM_LEADERBOARD,
optionsJson = NostrJsonEncodeDefaults.encodeToString(
optionsJson = NostrJsonImplicitNulls.encodeToString(
PremiumLeaderboardRequest(
orderBy = orderBy,
limit = limit,
since = since,
until = until,
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import kotlinx.serialization.Serializable
@Serializable
class PremiumLeaderboardRequest(
@SerialName("order_by") val orderBy: PremiumLeaderboardOrderBy,
val limit: Int = 100,
val limit: Int,
val since: Long?,
val until: Long?,
)

enum class PremiumLeaderboardOrderBy {
@SerialName("premium_since")
PremiumSince,

@SerialName("index")
Index,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package net.primal.android.premium.api.paging

import androidx.paging.PagingSource
import androidx.paging.PagingState
import net.primal.android.networking.sockets.errors.WssException
import net.primal.android.premium.leaderboard.domain.OGLeaderboardEntry
import net.primal.android.premium.repository.PremiumRepository

class PremiumLeaderboardPagingSource(
private val premiumRepository: PremiumRepository,
private val pageSize: Int,
) : PagingSource<Long, OGLeaderboardEntry>() {

override fun getRefreshKey(state: PagingState<Long, OGLeaderboardEntry>): Long? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.premiumSince?.minus(1)
?: state.closestPageToPosition(position)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Long>): LoadResult<Long, OGLeaderboardEntry> {
val currentUntil = params.key

return try {
val response = premiumRepository.fetchPremiumLeaderboard(
until = currentUntil,
limit = pageSize,
)

val nextUntil = if (response.isEmpty()) null else response.last().premiumSince?.minus(1)

LoadResult.Page(
data = response,
prevKey = null,
nextKey = nextUntil,
)
} catch (e: WssException) {
LoadResult.Error(e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ data class LeaderboardLegendEntry(
val displayName: String?,
val internetIdentifier: String?,
val premiumProfileDataUi: PremiumProfileDataUi?,
val donatedBtc: Double,
val donatedSats: ULong,
)
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class LegendLeaderboardViewModel @Inject constructor(
viewModelScope.launch {
setState { copy(loading = true, error = null) }
try {
val entries = premiumRepository.fetchLegendLeaderboard(orderBy = orderBy)
val entries = premiumRepository.fetchLegendLeaderboard(orderBy = orderBy, limit = 300)
setState { copy(leaderboardEntries = leaderboardEntries + (orderBy to entries)) }
} catch (error: WssException) {
Timber.w(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import net.primal.android.core.utils.formatToDefaultDateFormat
import net.primal.android.premium.leaderboard.domain.LeaderboardLegendEntry
import net.primal.android.premium.legend.domain.LegendaryCustomization
import net.primal.android.theme.AppTheme
import net.primal.android.wallet.utils.CurrencyConversionUtils.toSats

@Composable
fun LegendLeaderboardItem(
Expand Down Expand Up @@ -71,7 +70,7 @@ fun LegendLeaderboardItem(
displayName = item.displayName,
internetIdentifier = item.internetIdentifier,
legendaryCustomization = item.premiumProfileDataUi?.legendaryCustomization,
satsDonated = item.donatedBtc.toSats(),
satsDonated = item.donatedSats,
)
},
supportingContent = {
Expand Down Expand Up @@ -104,7 +103,7 @@ private fun DisplayNameAndSatsDonatedRow(
displayName: String?,
internetIdentifier: String?,
legendaryCustomization: LegendaryCustomization?,
satsDonated: Double,
satsDonated: ULong,
) {
val numberFormat = remember { NumberFormat.getNumberInstance() }

Expand All @@ -127,7 +126,7 @@ private fun DisplayNameAndSatsDonatedRow(
)
}
Text(
text = numberFormat.format(satsDonated),
text = numberFormat.format(satsDonated.toLong()),
fontWeight = FontWeight.Bold,
style = AppTheme.typography.bodyMedium,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package net.primal.android.premium.leaderboard.ogs

import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import net.primal.android.premium.leaderboard.domain.OGLeaderboardEntry

interface OGLeaderboardContract {
data class UiState(
val leaderboardEntries: List<OGLeaderboardEntry> = emptyList(),
val leaderboardEntries: Flow<PagingData<OGLeaderboardEntry>>,
val loading: Boolean = true,
val error: Throwable? = null,
val isActiveAccountPremium: Boolean = false,
)

sealed class UiEvent {
data object RetryFetch : UiEvent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
Expand All @@ -21,20 +21,26 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.launch
import net.primal.android.R
import net.primal.android.core.compose.HeightAdjustableLoadingLazyListPlaceholder
import net.primal.android.core.compose.ListNoContent
import net.primal.android.core.compose.PrimalDivider
import net.primal.android.core.compose.PrimalTopAppBar
import net.primal.android.core.compose.foundation.rememberLazyListStatePagingWorkaround
import net.primal.android.core.compose.icons.PrimalIcons
import net.primal.android.core.compose.icons.primaliconpack.ArrowBack
import net.primal.android.core.compose.isEmpty
import net.primal.android.premium.leaderboard.domain.OGLeaderboardEntry
import net.primal.android.premium.leaderboard.legend.ui.LATEST_INDEX
import net.primal.android.premium.leaderboard.ogs.ui.OGLeaderboardItem
import net.primal.android.premium.leaderboard.ogs.ui.OGLeaderboardTabs
import net.primal.android.premium.leaderboard.ogs.ui.PAGE_COUNT
import net.primal.android.theme.AppTheme
import timber.log.Timber

@Composable
fun OGLeaderboardScreen(
Expand All @@ -47,7 +53,6 @@ fun OGLeaderboardScreen(

OGLeaderboardScreen(
state = uiState.value,
eventPublisher = viewModel::setEvent,
onClose = onClose,
onProfileClick = onProfileClick,
onGetPrimalPremiumClick = onGetPrimalPremiumClick,
Expand All @@ -58,7 +63,6 @@ fun OGLeaderboardScreen(
@Composable
private fun OGLeaderboardScreen(
state: OGLeaderboardContract.UiState,
eventPublisher: (OGLeaderboardContract.UiEvent) -> Unit,
onClose: () -> Unit,
onGetPrimalPremiumClick: () -> Unit,
onProfileClick: (String) -> Unit,
Expand All @@ -75,38 +79,66 @@ private fun OGLeaderboardScreen(
)
},
) { paddingValues ->
val pagingItems = state.leaderboardEntries.collectAsLazyPagingItems()
val lazyListState = pagingItems.rememberLazyListStatePagingWorkaround()
HorizontalPager(
contentPadding = paddingValues,
state = pagerState,
) { currentPage ->
if (state.loading && state.leaderboardEntries.isEmpty()) {
HeightAdjustableLoadingLazyListPlaceholder(height = 80.dp)
} else if (state.error != null) {
ListNoContent(
modifier = Modifier.fillMaxSize(),
noContentText = stringResource(id = R.string.premium_leaderboard_no_content),
onRefresh = {
eventPublisher(OGLeaderboardContract.UiEvent.RetryFetch)
},
)
if (pagingItems.isEmpty()) {
when (val refreshLoadState = pagingItems.loadState.refresh) {
is LoadState.Error -> {
Timber.w(refreshLoadState.error)
ListNoContent(
modifier = Modifier.fillMaxSize(),
noContentText = stringResource(id = R.string.premium_leaderboard_error_loading),
onRefresh = { pagingItems.refresh() },
)
}

is LoadState.NotLoading -> {
if (pagingItems.loadState.isIdle) {
ListNoContent(
modifier = Modifier.fillMaxSize(),
noContentText = stringResource(id = R.string.premium_leaderboard_no_content),
onRefresh = { pagingItems.refresh() },
)
}
}

LoadState.Loading -> {
HeightAdjustableLoadingLazyListPlaceholder(height = 80.dp)
}
}
} else {
LeaderboardList(entries = state.leaderboardEntries, onProfileClick = onProfileClick)
LeaderboardList(
lazyListState = lazyListState,
entries = pagingItems,
onProfileClick = onProfileClick,
)
}
}
}
}

@Composable
private fun LeaderboardList(entries: List<OGLeaderboardEntry>, onProfileClick: (String) -> Unit) {
LazyColumn {
private fun LeaderboardList(
lazyListState: LazyListState,
entries: LazyPagingItems<OGLeaderboardEntry>,
onProfileClick: (String) -> Unit,
) {
LazyColumn(state = lazyListState) {
items(
items = entries,
key = { it.userId },
) { entry ->
OGLeaderboardItem(
item = entry,
onClick = { onProfileClick(entry.userId) },
)
count = entries.itemCount,
) { index ->
val entry = entries[index]

entry?.let {
OGLeaderboardItem(
item = entry,
onClick = { onProfileClick(entry.userId) },
)
}
}
}
}
Expand Down
Loading

0 comments on commit 314ae5c

Please sign in to comment.