From 0f8128ea74f46101b9dd08de9dbec8ab6c62fe86 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Mon, 2 Dec 2024 18:31:38 +0900 Subject: [PATCH] update profile screen ui --- .../flare/ui/screen/profile/ProfileScreen.kt | 161 +++++++++--------- app/src/main/res/values/strings.xml | 4 +- .../data/datasource/guest/GuestDataSource.kt | 35 +++- .../guest/GuestUserTimelinePagingSource.kt | 2 + .../microblog/MicroblogDataSource.kt | 1 + .../data/datasource/microblog/ProfileTab.kt | 13 +- .../ui/presenter/profile/ProfilePresenter.kt | 48 ++++++ 7 files changed, 177 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt index 3474ca78d..bc5cf83d5 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/profile/ProfileScreen.kt @@ -98,11 +98,10 @@ import compose.icons.fontawesomeicons.solid.VolumeXmark import dev.dimension.flare.R import dev.dimension.flare.common.AppDeepLink import dev.dimension.flare.common.PagingState -import dev.dimension.flare.common.isLoading -import dev.dimension.flare.common.isSuccess import dev.dimension.flare.common.onLoading import dev.dimension.flare.common.onSuccess import dev.dimension.flare.data.datasource.microblog.ProfileAction +import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.model.PlatformType @@ -608,68 +607,71 @@ private fun ProfileScreen( isRefreshing = state.isRefreshing, indicatorPadding = it, content = { - val pagerState = rememberPagerState { state.profileTabs.size } val content = @Composable { - Column { - Box { - if (state.profileTabs.size > 1) { - SecondaryScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - modifier = Modifier.fillMaxWidth(), - edgePadding = screenHorizontalPadding, - divider = {}, - ) { - state.profileTabs.forEachIndexed { index, profileTab -> - Tab( - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - ) { - Text( - profileTab.title, - modifier = - Modifier - .padding(8.dp), - ) + state.state.tabs.onSuccess { tabs -> + val pagerState = rememberPagerState { tabs.size } + Column { + Box { + if (tabs.size > 1) { + SecondaryScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + modifier = Modifier.fillMaxWidth(), + edgePadding = screenHorizontalPadding, + divider = {}, + ) { + tabs.toImmutableList().forEachIndexed { index, profileTab -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + ) { + Text( + profileTab.title, + modifier = + Modifier + .padding(8.dp), + ) + } } } } + HorizontalDivider( + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + ) } - HorizontalDivider( - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(), - ) - } - HorizontalPager( - state = pagerState, - ) { index -> - val type = state.profileTabs[index] - when (type) { - ProfileTab.Timeline -> - LazyStatusVerticalStaggeredGrid( - state = listState, - contentPadding = - PaddingValues( - top = 8.dp, - bottom = 8.dp + it.calculateBottomPadding(), - ), - modifier = Modifier.fillMaxSize(), - ) { - status(state.state.listState) + HorizontalPager( + state = pagerState, + ) { index -> + val type = tabs[index] + when (type) { + is ProfileState.Tab.Media -> { + ProfileMediaTab( + mediaState = type.data, + onItemClicked = { statusKey, index, preview -> + onMediaClick(statusKey, index, preview) + }, + modifier = Modifier.fillMaxSize(), + ) + } + is ProfileState.Tab.Timeline -> { + LazyStatusVerticalStaggeredGrid( + state = listState, + contentPadding = + PaddingValues( + top = 8.dp, + bottom = 8.dp + it.calculateBottomPadding(), + ), + modifier = Modifier.fillMaxSize(), + ) { + status(type.data) + } } - ProfileTab.Media -> { - ProfileMediaTab( - mediaState = state.state.mediaState, - onItemClicked = { statusKey, index, preview -> - onMediaClick(statusKey, index, preview) - }, - modifier = Modifier.fillMaxSize(), - ) } } } @@ -718,7 +720,9 @@ private fun ProfileScreen( ) } }, - content = content, + content = { + content.invoke() + }, ) } }, @@ -1563,17 +1567,16 @@ private fun profilePresenter( var showMoreMenus by remember { mutableStateOf(false) } - val mediaState = state.mediaState - - val profileTabs = - listOfNotNull( - ProfileTab.Timeline, - if (mediaState.isSuccess() && mediaState.itemCount > 0 || mediaState.isLoading) { - ProfileTab.Media - } else { - null - }, - ) +// val mediaState = state.mediaState +// val profileTabs = +// listOfNotNull( +// ProfileTab.Timeline, +// if (mediaState.isSuccess() && mediaState.itemCount > 0 || mediaState.isLoading) { +// ProfileTab.Media +// } else { +// null +// }, +// ) val allAccounts = remember { AccountsPresenter() @@ -1590,7 +1593,7 @@ private fun profilePresenter( } val showMoreMenus = showMoreMenus val isRefreshing = isRefreshing - val profileTabs = profileTabs +// val profileTabs = profileTabs fun setShowMoreMenus(value: Boolean) { showMoreMenus = value @@ -1606,15 +1609,15 @@ private fun profilePresenter( } } -private enum class ProfileTab { - Timeline, - Media, -} - -private val ProfileTab.title: String +private val ProfileState.Tab.title: String @Composable get() = when (this) { - ProfileTab.Timeline -> stringResource(R.string.profile_tab_timeline) - ProfileTab.Media -> stringResource(R.string.profile_tab_media) + is ProfileState.Tab.Timeline -> + when (type) { + ProfileTab.Timeline.Type.Status -> stringResource(R.string.profile_tab_timeline) + ProfileTab.Timeline.Type.StatusWithReplies -> stringResource(R.string.profile_tab_timeline_with_reply) + ProfileTab.Timeline.Type.Likes -> stringResource(R.string.profile_tab_likes) + } + is ProfileState.Tab.Media -> stringResource(R.string.profile_tab_media) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0274fe08..563b234eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -402,7 +402,9 @@ Unsubscribe Subscribe - Timeline + Posts + Posts and replies + Likes Media Tab settings diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDataSource.kt index 860ef7859..251c19ded 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestDataSource.kt @@ -16,6 +16,7 @@ import dev.dimension.flare.data.datasource.mastodon.SearchUserPagingSource import dev.dimension.flare.data.datasource.mastodon.TrendHashtagPagingSource import dev.dimension.flare.data.datasource.mastodon.TrendsUserPagingSource import dev.dimension.flare.data.datasource.microblog.MicroblogDataSource +import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.network.mastodon.GuestMastodonService import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.model.PlatformType @@ -25,6 +26,8 @@ import dev.dimension.flare.ui.model.UiTimeline import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.mapper.render import dev.dimension.flare.ui.model.mapper.renderGuest +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -108,7 +111,7 @@ object GuestDataSource : MicroblogDataSource, KoinComponent { userId = userKey.id, onlyMedia = mediaOnly, ) - }.flow + }.flow.cachedIn(scope) override fun context( statusKey: MicroBlogKey, @@ -225,4 +228,34 @@ object GuestDataSource : MicroblogDataSource, KoinComponent { accountKey = null, ) }.flow.cachedIn(scope) + + override fun profileTabs( + userKey: MicroBlogKey, + scope: CoroutineScope, + pagingSize: Int, + ): ImmutableList = + persistentListOf( + ProfileTab.Timeline( + type = ProfileTab.Timeline.Type.Status, + flow = + Pager(PagingConfig(pageSize = pagingSize)) { + GuestUserTimelinePagingSource( + host = GuestMastodonService.HOST, + userId = userKey.id, + ) + }.flow.cachedIn(scope), + ), + ProfileTab.Timeline( + type = ProfileTab.Timeline.Type.StatusWithReplies, + flow = + Pager(PagingConfig(pageSize = pagingSize)) { + GuestUserTimelinePagingSource( + host = GuestMastodonService.HOST, + userId = userKey.id, + withReply = true, + ) + }.flow.cachedIn(scope), + ), + ProfileTab.Media, + ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt index 0d2c814a6..e974ebc56 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/guest/GuestUserTimelinePagingSource.kt @@ -9,6 +9,7 @@ import dev.dimension.flare.ui.model.mapper.renderGuest internal class GuestUserTimelinePagingSource( private val host: String, private val userId: String, + private val withReply: Boolean = false, private val onlyMedia: Boolean = false, ) : PagingSource() { override fun getRefreshKey(state: PagingState): String? = null @@ -23,6 +24,7 @@ internal class GuestUserTimelinePagingSource( limit = limit, max_id = maxId, only_media = onlyMedia, + exclude_replies = !withReply, ) LoadResult.Page( data = diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt index d8c7d523b..5e1d62575 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/MicroblogDataSource.kt @@ -79,5 +79,6 @@ interface MicroblogDataSource { fun profileTabs( userKey: MicroBlogKey, scope: CoroutineScope, + pagingSize: Int = 20, ): ImmutableList } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt index d4edf2c9f..235167e40 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/microblog/ProfileTab.kt @@ -2,23 +2,24 @@ package dev.dimension.flare.data.datasource.microblog import androidx.compose.runtime.Immutable import androidx.paging.PagingData -import dev.dimension.flare.ui.model.UiMedia import dev.dimension.flare.ui.model.UiTimeline -import dev.dimension.flare.ui.presenter.profile.ProfileMedia import kotlinx.coroutines.flow.Flow +@Immutable sealed interface ProfileTab { + @Immutable data class Timeline( val type: Type, val flow: Flow>, ) : ProfileTab { + @Immutable enum class Type { Status, StatusWithReplies, + Likes, } } - data class Media( - val flow: Flow>, - ) : ProfileTab -} \ No newline at end of file + @Immutable + data object Media : ProfileTab +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt index 90f42e1e9..a46a10c35 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/profile/ProfilePresenter.kt @@ -13,6 +13,7 @@ import dev.dimension.flare.data.datasource.microblog.AuthenticatedMicroblogDataS import dev.dimension.flare.data.datasource.microblog.DirectMessageDataSource import dev.dimension.flare.data.datasource.microblog.ListDataSource import dev.dimension.flare.data.datasource.microblog.ProfileAction +import dev.dimension.flare.data.datasource.microblog.ProfileTab import dev.dimension.flare.data.repository.AccountRepository import dev.dimension.flare.data.repository.NoActiveAccountException import dev.dimension.flare.data.repository.accountServiceProvider @@ -137,6 +138,37 @@ class ProfilePresenter( } } }.flatMap { it.collectAsUiState().value } + + val tabs = + accountServiceState.map { service -> + val actualUserKey = + userKey + ?: if (service is AuthenticatedMicroblogDataSource) { + service.accountKey + } else { + null + } ?: throw NoActiveAccountException + remember(service, userKey) { + service.profileTabs(actualUserKey, scope) + }.map { + when (it) { + is ProfileTab.Media -> + ProfileState.Tab.Media( + data = mediaState, + ) + is ProfileTab.Timeline -> { + ProfileState.Tab.Timeline( + type = it.type, + data = it.flow.collectAsLazyPagingItems().toPagingState(), + ) + } + } + }.let { + remember(it) { + it.toImmutableList().toImmutableListWrapper() + } + } + } return object : ProfileState( userState = userState.flatMap { it.toUi() }, listState = listState, @@ -151,6 +183,7 @@ class ProfilePresenter( }, myAccountKey = myAccountKey, canSendMessage = canSendMessage, + tabs = tabs, ) { override suspend fun refresh() { userState.onSuccess { @@ -202,6 +235,7 @@ abstract class ProfileState( val isListDataSource: UiState, val myAccountKey: UiState, val canSendMessage: UiState, + val tabs: UiState>, ) { abstract suspend fun refresh() @@ -217,6 +251,20 @@ abstract class ProfileState( ) abstract fun report(userKey: MicroBlogKey) + + @Immutable + sealed interface Tab { + @Immutable + data class Timeline( + val type: ProfileTab.Timeline.Type, + val data: PagingState, + ) : Tab + + @Immutable + data class Media( + val data: PagingState, + ) : Tab + } } class ProfileWithUserNameAndHostPresenter(