From 89c87581e0a76e432d289584af31c7cca819c7d3 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Fri, 18 Oct 2024 00:35:33 +0900 Subject: [PATCH 1/4] add cross search for user --- .../flare/ui/screen/profile/ProfileScreen.kt | 155 ++++++++++++++++-- app/src/main/res/values/strings.xml | 2 + .../dev/dimension/flare/ui/model/UiProfile.kt | 7 + .../ui/presenter/profile/ProfilePresenter.kt | 11 ++ 4 files changed, 165 insertions(+), 10 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 bf7d20b7a..a1a836709 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 @@ -76,6 +76,7 @@ import com.ramcosta.composedestinations.annotation.parameters.DeepLink import com.ramcosta.composedestinations.annotation.parameters.FULL_ROUTE_PLACEHOLDER import com.ramcosta.composedestinations.generated.destinations.EditAccountListRouteDestination import com.ramcosta.composedestinations.generated.destinations.ProfileMediaRouteDestination +import com.ramcosta.composedestinations.generated.destinations.SearchRouteDestination import com.ramcosta.composedestinations.generated.destinations.StatusMediaRouteDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import compose.icons.FontAwesomeIcons @@ -126,6 +127,8 @@ import dev.dimension.flare.ui.presenter.profile.ProfileMedia import dev.dimension.flare.ui.presenter.profile.ProfilePresenter import dev.dimension.flare.ui.presenter.profile.ProfileState import dev.dimension.flare.ui.presenter.profile.ProfileWithUserNameAndHostPresenter +import dev.dimension.flare.ui.presenter.settings.AccountsPresenter +import dev.dimension.flare.ui.presenter.settings.AccountsState import dev.dimension.flare.ui.screen.home.RegisterTabCallback import dev.dimension.flare.ui.theme.MediumAlpha import dev.dimension.flare.ui.theme.screenHorizontalPadding @@ -195,6 +198,14 @@ internal fun ProfileWithUserNameAndHostDeeplinkRoute( ), ) }, + toSearchUserUsingAccount = { handle, accountKey -> + navigator.navigate( + SearchRouteDestination( + handle, + AccountType.Specific(accountKey), + ), + ) + }, ) }.onLoading { ProfileLoadingScreen( @@ -262,6 +273,14 @@ internal fun ProfileWithUserNameAndHostRoute( toEditAccountList = { navigator.navigate(EditAccountListRouteDestination(accountType, it.key)) }, + toSearchUserUsingAccount = { handle, accountKey -> + navigator.navigate( + SearchRouteDestination( + handle, + AccountType.Specific(accountKey), + ), + ) + }, ) }.onLoading { ProfileLoadingScreen( @@ -408,6 +427,14 @@ internal fun ProfileDeeplinkRoute( ), ) }, + toSearchUserUsingAccount = { handle, accountKey -> + navigator.navigate( + SearchRouteDestination( + handle, + AccountType.Specific(accountKey), + ), + ) + }, ) } @@ -456,6 +483,14 @@ internal fun ProfileRoute( navigator.navigate(EditAccountListRouteDestination(accountType, userKey)) } }, + toSearchUserUsingAccount = { handle, accountKey -> + navigator.navigate( + SearchRouteDestination( + handle, + AccountType.Specific(accountKey), + ), + ) + }, ) } @@ -466,6 +501,7 @@ internal fun ProfileRoute( private fun ProfileScreen( accountType: AccountType, toEditAccountList: () -> Unit, + toSearchUserUsingAccount: (String, MicroBlogKey) -> Unit, userKey: MicroBlogKey? = null, onBack: () -> Unit = {}, showBackButton: Boolean = true, @@ -574,6 +610,8 @@ private fun ProfileScreen( setShowMoreMenus = state::setShowMoreMenus, showMoreMenus = state.showMoreMenus, toEditAccountList = toEditAccountList, + accountsState = state.allAccountsState, + toSearchUserUsingAccount = toSearchUserUsingAccount, ) } }, @@ -608,6 +646,8 @@ private fun ProfileScreen( setShowMoreMenus = state::setShowMoreMenus, showMoreMenus = state.showMoreMenus, toEditAccountList = toEditAccountList, + accountsState = state.allAccountsState, + toSearchUserUsingAccount = toSearchUserUsingAccount, ) }, expandMatrices = true, @@ -621,6 +661,7 @@ private fun ProfileScreen( // it.banner?.let { it1 -> onMediaClick(it1) } } }, + isBigScreen = bigScreen, ) } } @@ -735,6 +776,7 @@ private fun ProfileScreen( // it.banner?.let { it1 -> onMediaClick(it1) } } }, + isBigScreen = bigScreen, ) } }, @@ -806,9 +848,11 @@ private fun ProfileMediaTab( @Composable private fun ProfileMenu( profileState: ProfileState, + accountsState: AccountsState, setShowMoreMenus: (Boolean) -> Unit, showMoreMenus: Boolean, toEditAccountList: () -> Unit, + toSearchUserUsingAccount: (String, MicroBlogKey) -> Unit, ) { profileState.isMe.onSuccess { isMe -> if (!isMe) { @@ -848,6 +892,42 @@ private fun ProfileMenu( } } } + accountsState.accounts.onSuccess { accounts -> + profileState.myAccountKey.onSuccess { myKey -> + if (accounts.size > 1) { + for (i in 0 until accounts.size) { + val account = accounts[i] + account.second.onSuccess { accountData -> + if (accountData.key != user.key && + accountData.key != myKey && + accountData.platformType != user.platformType + ) { + DropdownMenuItem( + text = { + Text( + text = + stringResource( + id = R.string.profile_search_user_using_account, + user.handleWithoutAtAndHost, + accountData.platformType.name, + accountData.handleWithoutAt, + ), + ) + }, + onClick = { + setShowMoreMenus(false) + toSearchUserUsingAccount( + user.handleWithoutAtAndHost, + accountData.key, + ) + }, + ) + } + } + } + } + } + } profileState.actions.onSuccess { actions -> for (i in 0.., menu: @Composable RowScope.() -> Unit, expandMatrices: Boolean, + isBigScreen: Boolean, modifier: Modifier = Modifier, ) { when (userState) { @@ -948,6 +1029,7 @@ private fun ProfileHeader( expandMatrices = expandMatrices, onAvatarClick = onAvatarClick, onBannerClick = onBannerClick, + isBigScreen = isBigScreen, ) } } @@ -962,6 +1044,7 @@ private fun ProfileHeaderSuccess( onBannerClick: () -> Unit, isMe: UiState, menu: @Composable RowScope.() -> Unit, + isBigScreen: Boolean, modifier: Modifier = Modifier, expandMatrices: Boolean = false, ) { @@ -972,6 +1055,7 @@ private fun ProfileHeaderSuccess( displayName = user.name.data, userKey = user.key, handle = user.handle, + isBigScreen = isBigScreen, headerTrailing = { isMe.onSuccess { if (!it) { @@ -1144,6 +1228,7 @@ internal fun CommonProfileHeader( displayName: Element, userKey: MicroBlogKey, handle: String, + isBigScreen: Boolean, modifier: Modifier = Modifier, onAvatarClick: (() -> Unit)? = null, onBannerClick: (() -> Unit)? = null, @@ -1251,11 +1336,60 @@ internal fun CommonProfileHeader( }, ) } - Column( + if (!isBigScreen) { + Column( + modifier = + Modifier + .weight(1f) + .padding(top = actualBannerHeight), + ) { + HtmlText( + element = displayName, + textStyle = MaterialTheme.typography.titleMedium, +// modifier = +// Modifier +// .sharedElement( +// rememberSharedContentState(key = "profile-display-name-$userKey"), +// animatedVisibilityScope = this@AnimatedVisibilityScope, +// ), + ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = handle, + style = MaterialTheme.typography.bodySmall, +// modifier = +// Modifier +// .sharedElement( +// rememberSharedContentState(key = "profile-handle-$userKey"), +// animatedVisibilityScope = this@AnimatedVisibilityScope, +// ), + ) + handleTrailing.invoke(this) + } + } + } else { + Spacer( + modifier = + Modifier + .weight(1f), + ) + } + Row( modifier = Modifier - .weight(1f) .padding(top = actualBannerHeight), + ) { + headerTrailing() + } + } + if (isBigScreen) { + Column( + modifier = + Modifier + .padding(horizontal = screenHorizontalPadding), ) { HtmlText( element = displayName, @@ -1284,13 +1418,6 @@ internal fun CommonProfileHeader( handleTrailing.invoke(this) } } - Row( - modifier = - Modifier - .padding(top = actualBannerHeight), - ) { - headerTrailing() - } } // content Box { @@ -1387,7 +1514,10 @@ internal fun ProfileHeaderLoading( } Text( text = "Lorem Ipsum is simply dummy text", - modifier = Modifier.placeholder(true), + modifier = + Modifier + .placeholder(true) + .padding(horizontal = screenHorizontalPadding), ) } } @@ -1421,8 +1551,13 @@ private fun profilePresenter( null }, ) + val allAccounts = + remember { + AccountsPresenter() + }.invoke() object { val state = state + val allAccountsState = allAccounts val showMoreMenus = showMoreMenus val isRefreshing = isRefreshing val profileTabs = profileTabs diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60acc4a19..585348353 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -669,4 +669,6 @@ Voted Expired at %1$s + Find %1$s in %2$s using %3$s + \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt index 71edbb3b5..c072fe8ad 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt @@ -37,6 +37,13 @@ data class UiProfile internal constructor( val statusesCountHumanized = statusesCount.humanize() } + val handleWithoutAt = handle.substringAfter("@") + + val handleWithoutAtAndHost = + handle + .substringBeforeLast("@") + .substringAfter("@") + sealed interface BottomContent { data class Fields( val fields: ImmutableMap, 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 72db2fdc1..593c47d34 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 @@ -23,6 +23,7 @@ import dev.dimension.flare.ui.model.UiUserV2 import dev.dimension.flare.ui.model.collectAsUiState import dev.dimension.flare.ui.model.flatMap import dev.dimension.flare.ui.model.map +import dev.dimension.flare.ui.model.mapNotNull import dev.dimension.flare.ui.model.onSuccess import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase @@ -106,6 +107,14 @@ class ProfilePresenter( accountServiceState.map { service -> service.profileActions().toImmutableList().toImmutableListWrapper() } + val myAccountKey = + accountServiceState.mapNotNull { + if (it is AuthenticatedMicroblogDataSource) { + it.accountKey + } else { + null + } + } return object : ProfileState( userState = userState.flatMap { it.toUi() }, listState = listState, @@ -118,6 +127,7 @@ class ProfilePresenter( accountServiceState.map { it is ListDataSource }, + myAccountKey = myAccountKey, ) { override suspend fun refresh() { userState.onSuccess { @@ -186,6 +196,7 @@ abstract class ProfileState( val actions: UiState>, val isGuestMode: Boolean, val isListDataSource: UiState, + val myAccountKey: UiState, ) { abstract suspend fun refresh() From c6e8e183209a9e03b50a42464cda826c77d50470 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Fri, 18 Oct 2024 00:46:15 +0900 Subject: [PATCH 2/4] remove underline for bluesky --- .../dimension/flare/ui/screen/profile/ProfileScreen.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 a1a836709..2798ed919 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,6 +98,7 @@ import dev.dimension.flare.common.onSuccess import dev.dimension.flare.data.datasource.microblog.ProfileAction import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey +import dev.dimension.flare.model.PlatformType import dev.dimension.flare.molecule.producePresenter import dev.dimension.flare.ui.common.plus import dev.dimension.flare.ui.component.AvatarComponent @@ -916,8 +917,15 @@ private fun ProfileMenu( }, onClick = { setShowMoreMenus(false) + val actualHandle = + if (accountData.platformType == PlatformType.Bluesky) { + user.handleWithoutAtAndHost + .replace("_", "") + } else { + user.handleWithoutAtAndHost + } toSearchUserUsingAccount( - user.handleWithoutAtAndHost, + actualHandle, accountData.key, ) }, From 61afbf8412cfb5541e4c28a078cff7c387c8c337 Mon Sep 17 00:00:00 2001 From: Tlaster Date: Fri, 18 Oct 2024 01:15:17 +0900 Subject: [PATCH 3/4] fix user search --- .../component/status/UiTimelineComponent.kt | 11 ++++++++- .../mastodon/SearchUserPagingSource.kt | 6 ++--- .../dev/dimension/flare/ui/model/UiProfile.kt | 23 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/dev/dimension/flare/ui/component/status/UiTimelineComponent.kt b/app/src/main/java/dev/dimension/flare/ui/component/status/UiTimelineComponent.kt index dd01db1f6..b5c6d3d6f 100644 --- a/app/src/main/java/dev/dimension/flare/ui/component/status/UiTimelineComponent.kt +++ b/app/src/main/java/dev/dimension/flare/ui/component/status/UiTimelineComponent.kt @@ -98,6 +98,7 @@ private fun UserListContent( data: UiTimeline.ItemContent.UserList, modifier: Modifier = Modifier, ) { + val uriHandler = LocalUriHandler.current Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp), @@ -112,7 +113,15 @@ private fun UserListContent( ) { CommonStatusHeaderComponent( data = user, - onUserClick = {}, + onUserClick = { + user.onClicked.invoke( + ClickContext( + launcher = { + uriHandler.openUri(it) + }, + ), + ) + }, modifier = Modifier.padding(8.dp), ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt index 0f05b0bf8..cc8e3246b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/mastodon/SearchUserPagingSource.kt @@ -28,11 +28,11 @@ internal class SearchUserPagingSource( following = following, resolve = resolve, ).accounts - ?.let { + ?.let { accounts -> return LoadResult.Page( - data = it.map { it.render(accountKey = accountKey, host = host) }, + data = accounts.map { it.render(accountKey = accountKey, host = host) }, prevKey = null, - nextKey = it.lastOrNull()?.id?.takeIf { it != params.key }, + nextKey = accounts.lastOrNull()?.id?.takeIf { it != params.key && accounts.size == params.loadSize }, ) } ?: run { return LoadResult.Error(Exception("No data")) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt index c072fe8ad..f2011d91c 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/UiProfile.kt @@ -37,12 +37,25 @@ data class UiProfile internal constructor( val statusesCountHumanized = statusesCount.humanize() } - val handleWithoutAt = handle.substringAfter("@") + val handleWithoutAt by lazy { + handle.removePrefix("@") + } - val handleWithoutAtAndHost = - handle - .substringBeforeLast("@") - .substringAfter("@") + val handleWithoutAtAndHost by lazy { + run { + handle + .removePrefix("@") + .split("@") + .firstOrNull() + ?: handleWithoutAt + }.let { + if (platformType == PlatformType.Bluesky) { + it.removeSuffix(".bsky.social") + } else { + it + } + } + } sealed interface BottomContent { data class Fields( From f0fdf37e3058713670dae476a069a626cdce40ab Mon Sep 17 00:00:00 2001 From: Tlaster Date: Fri, 18 Oct 2024 10:04:27 +0900 Subject: [PATCH 4/4] add badge support for vvo --- .../flare/common/ComposeInAppNotification.kt | 19 ++++--- app/src/main/res/values/strings.xml | 2 + .../flare/common/InAppNotification.kt | 1 + .../datasource/vvo/CommentPagingSource.kt | 4 ++ .../vvo/HomeTimelineRemoteMediator.kt | 7 +++ .../data/datasource/vvo/LikePagingSource.kt | 4 ++ .../datasource/vvo/MentionRemoteMediator.kt | 2 + .../data/datasource/vvo/VVODataSource.kt | 52 +++++++++++++++++++ .../data/network/nodeinfo/NodeInfoService.kt | 3 +- .../flare/data/network/vvo/api/ConfigApi.kt | 9 ++++ .../data/network/vvo/model/TimelineData.kt | 22 ++++++++ .../dev/dimension/flare/model/PlatformType.kt | 6 +++ 12 files changed, 120 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/dev/dimension/flare/common/ComposeInAppNotification.kt b/app/src/main/java/dev/dimension/flare/common/ComposeInAppNotification.kt index 6c1a82ece..6e475464e 100644 --- a/app/src/main/java/dev/dimension/flare/common/ComposeInAppNotification.kt +++ b/app/src/main/java/dev/dimension/flare/common/ComposeInAppNotification.kt @@ -34,21 +34,20 @@ internal class ComposeInAppNotification : InAppNotification { } override fun onSuccess(message: Message) { - val messageId = - when (message) { - Message.Compose -> R.string.compose_notification_success_title - } - _source.value = Event(Notification.StringNotification(messageId, success = true)) + _source.value = Event(Notification.StringNotification(message.title, success = true)) } override fun onError( message: Message, throwable: Throwable, ) { - val messageId = - when (message) { - Message.Compose -> R.string.compose_notification_error_title - } - _source.value = Event(Notification.StringNotification(messageId, success = false)) + _source.value = Event(Notification.StringNotification(message.title, success = false)) } } + +private val Message.title + get() = + when (this) { + Message.Compose -> R.string.compose_notification_title + Message.LoginExpired -> R.string.notification_login_expired + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 585348353..6124feadb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -671,4 +671,6 @@ Find %1$s in %2$s using %3$s + Login expired, please login again + \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/InAppNotification.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/InAppNotification.kt index b17c2621d..b25b4cdfe 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/common/InAppNotification.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/InAppNotification.kt @@ -17,4 +17,5 @@ interface InAppNotification { enum class Message { Compose, + LoginExpired, } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt index d744b4695..54ee35770 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/CommentPagingSource.kt @@ -13,6 +13,7 @@ internal class CommentPagingSource( private val service: VVOService, private val event: StatusEvent.VVO, private val accountKey: MicroBlogKey, + private val onClearMarker: () -> Unit, ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { return try { @@ -22,6 +23,9 @@ internal class CommentPagingSource( LoginExpiredException, ) } + if (params.key == null) { + onClearMarker.invoke() + } val response = service.getComments( page = params.key ?: 1, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt index fbcecf46c..3843c92f0 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/HomeTimelineRemoteMediator.kt @@ -4,6 +4,8 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator +import dev.dimension.flare.common.InAppNotification +import dev.dimension.flare.common.Message import dev.dimension.flare.data.database.cache.CacheDatabase import dev.dimension.flare.data.database.cache.mapper.VVO import dev.dimension.flare.data.database.cache.model.DbPagingTimelineView @@ -17,6 +19,7 @@ internal class HomeTimelineRemoteMediator( private val database: CacheDatabase, private val accountKey: MicroBlogKey, private val pagingKey: String, + private val inAppNotification: InAppNotification, ) : RemoteMediator() { override suspend fun initialize(): InitializeAction = InitializeAction.SKIP_INITIAL_REFRESH @@ -27,6 +30,10 @@ internal class HomeTimelineRemoteMediator( return try { val config = service.config() if (config.data?.login != true) { + inAppNotification.onError( + Message.LoginExpired, + LoginExpiredException, + ) return MediatorResult.Error( LoginExpiredException, ) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt index 3e9df7062..45b9699fc 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/LikePagingSource.kt @@ -13,6 +13,7 @@ internal class LikePagingSource( private val service: VVOService, private val event: StatusEvent.VVO, private val accountKey: MicroBlogKey, + private val onClearMarker: () -> Unit, ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { return try { @@ -22,6 +23,9 @@ internal class LikePagingSource( LoginExpiredException, ) } + if (params.key == null) { + onClearMarker.invoke() + } val response = service.getAttitudes( page = params.key ?: 1, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt index a2d1ad5de..fb76c0c00 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/MentionRemoteMediator.kt @@ -17,6 +17,7 @@ internal class MentionRemoteMediator( private val database: CacheDatabase, private val accountKey: MicroBlogKey, private val pagingKey: String, + private val onClearMarker: () -> Unit, ) : RemoteMediator() { var page = 1 @@ -40,6 +41,7 @@ internal class MentionRemoteMediator( page = page, ).also { database.pagingTimelineDao().delete(pagingKey = pagingKey, accountKey = accountKey) + onClearMarker.invoke() } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt index 8ca8d42d6..4d21d1c71 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/vvo/VVODataSource.kt @@ -8,6 +8,7 @@ import androidx.paging.cachedIn import dev.dimension.flare.common.CacheData import dev.dimension.flare.common.Cacheable import dev.dimension.flare.common.FileItem +import dev.dimension.flare.common.InAppNotification import dev.dimension.flare.common.MemCacheable import dev.dimension.flare.common.decodeJson import dev.dimension.flare.data.database.cache.CacheDatabase @@ -41,8 +42,10 @@ import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.compose.ComposeStatus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch +import kotlinx.datetime.Clock import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -56,6 +59,7 @@ class VVODataSource( private val database: CacheDatabase by inject() private val localFilterRepository: LocalFilterRepository by inject() private val coroutineScope: CoroutineScope by inject() + private val inAppNotification: InAppNotification by inject() private val service by lazy { VVOService(credential.chocolate) } @@ -78,6 +82,7 @@ class VVODataSource( database, accountKey, pagingKey, + inAppNotification, ), ) @@ -103,6 +108,9 @@ class VVODataSource( database, accountKey, pagingKey, + onClearMarker = { + MemCacheable.update(notificationMarkerMentionKey, 0) + }, ), ) @@ -114,6 +122,9 @@ class VVODataSource( service = service, accountKey = accountKey, event = this, + onClearMarker = { + MemCacheable.update(notificationMarkerCommentKey, 0) + }, ) }.flow.cachedIn(scope) @@ -125,6 +136,9 @@ class VVODataSource( service = service, accountKey = accountKey, event = this, + onClearMarker = { + MemCacheable.update(notificationMarkerLikeKey, 0) + }, ) }.flow.cachedIn(scope) } @@ -715,4 +729,42 @@ class VVODataSource( } } } + + private val notificationMarkerMentionKey: String + get() = "notificationBadgeCount_mention_$accountKey" + + private val notificationMarkerCommentKey: String + get() = "notificationBadgeCount_comment_$accountKey" + + private val notificationMarkerLikeKey: String + get() = "notificationBadgeCount_like_$accountKey" + + override fun notificationBadgeCount(): CacheData = + Cacheable( + fetchSource = { + val config = service.config() + val st = config.data?.st + requireNotNull(st) { "st is null" } + val response = + service.remindUnread( + time = Clock.System.now().toEpochMilliseconds() / 1000, + st = st, + ) + val mention = response.data?.mentionStatus ?: 0 + val comment = response.data?.cmt ?: 0 + val like = response.data?.attitude ?: 0 + + MemCacheable.update(notificationMarkerMentionKey, mention) + MemCacheable.update(notificationMarkerCommentKey, comment) + MemCacheable.update(notificationMarkerLikeKey, like) + }, + cacheSource = { + val mentionFlow = MemCacheable.subscribe(notificationMarkerMentionKey) + val commentFlow = MemCacheable.subscribe(notificationMarkerCommentKey) + val likeFlow = MemCacheable.subscribe(notificationMarkerLikeKey) + combine(mentionFlow, commentFlow, likeFlow) { mention, comment, like -> + (mention + comment + like).toInt() + } + }, + ) } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/nodeinfo/NodeInfoService.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/nodeinfo/NodeInfoService.kt index 7ae5abe86..d550e7d54 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/nodeinfo/NodeInfoService.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/nodeinfo/NodeInfoService.kt @@ -10,6 +10,7 @@ import dev.dimension.flare.data.network.nodeinfo.model.Schema21 import dev.dimension.flare.model.PlatformType import dev.dimension.flare.model.vvo import dev.dimension.flare.model.vvoHost +import dev.dimension.flare.model.vvoHostLong import dev.dimension.flare.model.vvoHostShort import dev.dimension.flare.model.xqtHost import io.ktor.client.call.body @@ -79,7 +80,7 @@ internal data object NodeInfoService { if (host.equals(xqtHost, ignoreCase = true) || host.equals("x.social", ignoreCase = true)) { return@coroutineScope PlatformType.xQt } - val vvo = listOf(vvoHost, vvo, vvoHostShort, "vvo.social") + val vvo = listOf(vvoHost, vvo, vvoHostShort, "vvo.social", vvoHostLong) if (vvo.any { it.equals(host, ignoreCase = true) }) { return@coroutineScope PlatformType.VVo } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/api/ConfigApi.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/api/ConfigApi.kt index 075133a54..14dfbb88b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/api/ConfigApi.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/api/ConfigApi.kt @@ -1,10 +1,19 @@ package dev.dimension.flare.data.network.vvo.api import de.jensklingenberg.ktorfit.http.GET +import de.jensklingenberg.ktorfit.http.Header +import de.jensklingenberg.ktorfit.http.Query import dev.dimension.flare.data.network.vvo.model.Config +import dev.dimension.flare.data.network.vvo.model.UnreadData import dev.dimension.flare.data.network.vvo.model.VVOResponse internal interface ConfigApi { @GET("api/config") suspend fun config(): VVOResponse + + @GET("api/remind/unread") + suspend fun remindUnread( + @Query("t") time: Long, + @Header("X-Xsrf-Token") st: String, + ): VVOResponse } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/model/TimelineData.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/model/TimelineData.kt index 737a09cf9..ca8e21409 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/model/TimelineData.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/network/vvo/model/TimelineData.kt @@ -564,3 +564,25 @@ data class UploadResponse( @SerialName("original_pic") val originalPic: String? = null, ) + +@Serializable +data class UnreadData( + val cmt: Long? = null, + val status: Long? = null, + val follower: Long? = null, + val dm: Long? = null, + @SerialName("mention_cmt") + val mentionCmt: Long? = null, + @SerialName("mention_status") + val mentionStatus: Long? = null, + val attitude: Long? = null, + val unreadmblog: Long? = null, + val uid: String? = null, + val bi: Long? = null, + val newfans: Long? = null, + val unreadmsg: Map? = null, +// val group: Any? = null, + val notice: Long? = null, + val photo: Long? = null, + val msgbox: Long? = null, +) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt index 0323422ab..ff3dbbb5b 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/model/PlatformType.kt @@ -59,3 +59,9 @@ val vvoHostShort: String = append(vvo) append("LmNu".decodeBase64String()) } + +val vvoHostLong: String = + buildString { + append("d2Vp".decodeBase64String()) + append("Ym8uY29t".decodeBase64String()) + }