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()) + }