diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt index 82109e02f..c88662364 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/database/cache/mapper/Bluesky.kt @@ -1,8 +1,11 @@ package dev.dimension.flare.data.database.cache.mapper +import SnowflakeIdGenerator import app.bsky.actor.ProfileView import app.bsky.actor.ProfileViewBasic import app.bsky.actor.ProfileViewDetailed +import app.bsky.bookmark.BookmarkView +import app.bsky.bookmark.BookmarkViewItemUnion import app.bsky.feed.FeedViewPost import app.bsky.feed.FeedViewPostReasonUnion import app.bsky.feed.Like @@ -10,7 +13,7 @@ import app.bsky.feed.PostView import app.bsky.feed.ReplyRefParentUnion import app.bsky.feed.Repost import app.bsky.notification.ListNotificationsNotification -import app.bsky.notification.ListNotificationsReason +import app.bsky.notification.ListNotificationsNotificationReason import chat.bsky.convo.ConvoView import chat.bsky.convo.ConvoViewLastMessageUnion import chat.bsky.convo.MessageView @@ -149,6 +152,25 @@ internal object Bluesky { } } +internal suspend fun List.toDb( + accountKey: MicroBlogKey, + pagingKey: String, + sortIdProvider: suspend (BookmarkView) -> Long = { + it.createdAt?.toStdlibInstant()?.toEpochMilliseconds() ?: SnowflakeIdGenerator.nextId() + }, +): List = + this.mapNotNull { + it.toDbStatusWithUser(accountKey)?.let { status -> + createDbPagingTimelineWithStatus( + accountKey = accountKey, + pagingKey = pagingKey, + sortId = sortIdProvider(it), + status = status, + references = mapOf(), + ) + } + } + internal fun List.toDb( accountKey: MicroBlogKey, pagingKey: String, @@ -173,10 +195,10 @@ internal fun List.toDb( val grouped = this.groupBy { it.reason }.filter { it.value.any() } return grouped.flatMap { (reason, items) -> when (reason) { - is ListNotificationsReason.Unknown, - ListNotificationsReason.StarterpackJoined, - ListNotificationsReason.Verified, - ListNotificationsReason.Unverified, + is ListNotificationsNotificationReason.Unknown, + ListNotificationsNotificationReason.StarterpackJoined, + ListNotificationsNotificationReason.Verified, + ListNotificationsNotificationReason.Unverified, -> items.map { createDbPagingTimelineWithStatus( @@ -188,15 +210,15 @@ internal fun List.toDb( ) } - ListNotificationsReason.Repost, ListNotificationsReason.Like -> { + ListNotificationsNotificationReason.Repost, ListNotificationsNotificationReason.Like -> { val post = items .first() .record .let { when (reason) { - ListNotificationsReason.Repost -> it.decodeAs().subject - ListNotificationsReason.Like -> it.decodeAs().subject + ListNotificationsNotificationReason.Repost -> it.decodeAs().subject + ListNotificationsNotificationReason.Like -> it.decodeAs().subject else -> null } }?.uri @@ -210,8 +232,8 @@ internal fun List.toDb( ) val idSuffix = when (reason) { - ListNotificationsReason.Repost -> "_repost" - ListNotificationsReason.Like -> "_like" + ListNotificationsNotificationReason.Repost -> "_repost" + ListNotificationsNotificationReason.Like -> "_like" else -> "" } val data = @@ -246,13 +268,18 @@ internal fun List.toDb( listOfNotNull( post, ).associate { - ReferenceType.Notification to listOfNotNull(it.toDbStatusWithUser(accountKey = accountKey)) + ReferenceType.Notification to + listOfNotNull( + it.toDbStatusWithUser( + accountKey = accountKey, + ), + ) }, ), ) } - ListNotificationsReason.Follow -> { + ListNotificationsNotificationReason.Follow -> { val content = UserList(data = items, post = null) val data = DbStatusWithUser( @@ -287,7 +314,10 @@ internal fun List.toDb( ) } - ListNotificationsReason.Mention, ListNotificationsReason.Reply, ListNotificationsReason.Quote -> { + ListNotificationsNotificationReason.Mention, + ListNotificationsNotificationReason.Reply, + ListNotificationsNotificationReason.Quote, + -> { items.mapNotNull { val post = references[it.uri] ?: return@mapNotNull null val content = Post(post = post) @@ -316,13 +346,18 @@ internal fun List.toDb( status = data, references = mapOf( - ReferenceType.Notification to listOfNotNull(post.toDbStatusWithUser(accountKey)), + ReferenceType.Notification to + listOfNotNull( + post.toDbStatusWithUser( + accountKey, + ), + ), ), ) } } - ListNotificationsReason.LikeViaRepost -> + ListNotificationsNotificationReason.LikeViaRepost -> items.mapNotNull { val post = references[it.uri] ?: return@mapNotNull null val content = Post(post = post) @@ -351,12 +386,17 @@ internal fun List.toDb( status = data, references = mapOf( - ReferenceType.Notification to listOfNotNull(post.toDbStatusWithUser(accountKey)), + ReferenceType.Notification to + listOfNotNull( + post.toDbStatusWithUser( + accountKey, + ), + ), ), ) } - ListNotificationsReason.RepostViaRepost -> + ListNotificationsNotificationReason.RepostViaRepost -> items.mapNotNull { val post = references[it.uri] ?: return@mapNotNull null val content = Post(post = post) @@ -385,12 +425,17 @@ internal fun List.toDb( status = data, references = mapOf( - ReferenceType.Notification to listOfNotNull(post.toDbStatusWithUser(accountKey)), + ReferenceType.Notification to + listOfNotNull( + post.toDbStatusWithUser( + accountKey, + ), + ), ), ) } - ListNotificationsReason.SubscribedPost -> { + ListNotificationsNotificationReason.SubscribedPost -> { items.mapNotNull { val post = references[it.uri] ?: return@mapNotNull null val content = Post(post = post) @@ -419,7 +464,12 @@ internal fun List.toDb( status = data, references = mapOf( - ReferenceType.Notification to listOfNotNull(post.toDbStatusWithUser(accountKey)), + ReferenceType.Notification to + listOfNotNull( + post.toDbStatusWithUser( + accountKey, + ), + ), ), ) } @@ -428,6 +478,14 @@ internal fun List.toDb( } } +private fun BookmarkView.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithUser? = + when (val content = item) { + is BookmarkViewItemUnion.BlockedPost -> null + is BookmarkViewItemUnion.NotFoundPost -> null + is BookmarkViewItemUnion.PostView -> content.value.toDbStatusWithUser(accountKey) + is BookmarkViewItemUnion.Unknown -> null + } + private fun ListNotificationsNotification.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithUser { val user = this.author.toDbUser(accountKey.host) val status = this.toDbStatus(accountKey) diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt index 245ac4597..5985b30a9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BlueskyDataSource.kt @@ -12,7 +12,7 @@ import app.bsky.actor.GetProfileQueryParams import app.bsky.actor.PreferencesUnion import app.bsky.actor.PutPreferencesRequest import app.bsky.actor.SavedFeed -import app.bsky.actor.Type +import app.bsky.actor.SavedFeedType import app.bsky.embed.Images import app.bsky.embed.ImagesImage import app.bsky.embed.Record @@ -1190,7 +1190,7 @@ internal class BlueskyDataSource( ?.value ?.items ?.filter { - it.type == Type.Feed + it.type == SavedFeedType.Feed }.orEmpty() service .getFeedGenerators( @@ -1325,7 +1325,7 @@ internal class BlueskyDataSource( ( pref.value.items + SavedFeed( - type = Type.Feed, + type = SavedFeedType.Feed, value = feedInfo.view.uri.atUri, pinned = true, id = Uuid.random().toString(), diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt new file mode 100644 index 000000000..a23aea899 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/BookmarkTimelineRemoteMediator.kt @@ -0,0 +1,63 @@ +package dev.dimension.flare.data.datasource.bluesky + +import androidx.paging.ExperimentalPagingApi +import app.bsky.bookmark.GetBookmarksQueryParams +import dev.dimension.flare.common.BaseTimelineRemoteMediator +import dev.dimension.flare.data.database.cache.CacheDatabase +import dev.dimension.flare.data.database.cache.mapper.toDb +import dev.dimension.flare.data.network.bluesky.BlueskyService +import dev.dimension.flare.model.MicroBlogKey + +@OptIn(ExperimentalPagingApi::class) +internal class BookmarkTimelineRemoteMediator( + private val service: BlueskyService, + database: CacheDatabase, + private val accountKey: MicroBlogKey, +) : BaseTimelineRemoteMediator( + database = database, + ) { + override val pagingKey: String = "bookmark_$accountKey" + + override suspend fun timeline( + pageSize: Int, + request: Request, + ): Result { + val response = + when (request) { + Request.Refresh -> { + service + .getBookmarks( + GetBookmarksQueryParams( + limit = pageSize.toLong(), + ), + ).requireResponse() + } + + is Request.Prepend -> { + return Result( + endOfPaginationReached = true, + ) + } + + is Request.Append -> { + service + .getBookmarks( + GetBookmarksQueryParams( + limit = pageSize.toLong(), + cursor = request.nextKey, + ), + ).requireResponse() + } + } + + return Result( + endOfPaginationReached = response.bookmarks.isEmpty() || response.cursor == null, + data = + response.bookmarks.toDb( + accountKey = accountKey, + pagingKey = pagingKey, + ), + nextKey = response.cursor, + ) + } +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt index f72ecf4c6..def7a47e9 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/NotificationRemoteMediator.kt @@ -4,8 +4,8 @@ import androidx.paging.ExperimentalPagingApi import app.bsky.feed.GetPostsQueryParams import app.bsky.feed.Like import app.bsky.feed.Repost +import app.bsky.notification.ListNotificationsNotificationReason import app.bsky.notification.ListNotificationsQueryParams -import app.bsky.notification.ListNotificationsReason import app.bsky.notification.UpdateSeenRequest import dev.dimension.flare.common.BaseTimelineRemoteMediator import dev.dimension.flare.data.database.cache.CacheDatabase @@ -79,27 +79,27 @@ internal class NotificationRemoteMediator( response.notifications .mapNotNull { when (it.reason) { - is ListNotificationsReason.Unknown -> null - ListNotificationsReason.Like -> + is ListNotificationsNotificationReason.Unknown -> null + ListNotificationsNotificationReason.Like -> it.record .decodeAs() .subject.uri - ListNotificationsReason.Repost -> + ListNotificationsNotificationReason.Repost -> it.record .decodeAs() .subject.uri - ListNotificationsReason.Follow -> null - ListNotificationsReason.Mention -> it.uri - ListNotificationsReason.Reply -> it.uri - ListNotificationsReason.Quote -> it.uri - ListNotificationsReason.StarterpackJoined -> null - ListNotificationsReason.Unverified -> null - ListNotificationsReason.Verified -> null - ListNotificationsReason.LikeViaRepost -> it.uri - ListNotificationsReason.RepostViaRepost -> it.uri - ListNotificationsReason.SubscribedPost -> it.uri + ListNotificationsNotificationReason.Follow -> null + ListNotificationsNotificationReason.Mention -> it.uri + ListNotificationsNotificationReason.Reply -> it.uri + ListNotificationsNotificationReason.Quote -> it.uri + ListNotificationsNotificationReason.StarterpackJoined -> null + ListNotificationsNotificationReason.Unverified -> null + ListNotificationsNotificationReason.Verified -> null + ListNotificationsNotificationReason.LikeViaRepost -> it.uri + ListNotificationsNotificationReason.RepostViaRepost -> it.uri + ListNotificationsNotificationReason.SubscribedPost -> it.uri } }.distinct() .toImmutableList() diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt index 4f89df863..feb4dfb85 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/model/mapper/Bluesky.kt @@ -13,7 +13,7 @@ import app.bsky.feed.Post import app.bsky.feed.PostView import app.bsky.feed.PostViewEmbedUnion import app.bsky.graph.ListView -import app.bsky.notification.ListNotificationsReason +import app.bsky.notification.ListNotificationsNotificationReason import app.bsky.richtext.Facet import app.bsky.richtext.FacetFeatureUnion import chat.bsky.convo.MessageView @@ -345,40 +345,40 @@ internal fun StatusContent.BlueskyNotification.renderBlueskyNotification( } } -private val ListNotificationsReason.icon: UiTimeline.TopMessage.Icon +private val ListNotificationsNotificationReason.icon: UiTimeline.TopMessage.Icon get() = when (this) { - ListNotificationsReason.Like -> UiTimeline.TopMessage.Icon.Favourite - ListNotificationsReason.Repost -> UiTimeline.TopMessage.Icon.Retweet - ListNotificationsReason.Follow -> UiTimeline.TopMessage.Icon.Follow - ListNotificationsReason.Mention -> UiTimeline.TopMessage.Icon.Mention - ListNotificationsReason.Reply -> UiTimeline.TopMessage.Icon.Reply - ListNotificationsReason.Quote -> UiTimeline.TopMessage.Icon.Reply - is ListNotificationsReason.Unknown -> UiTimeline.TopMessage.Icon.Info - ListNotificationsReason.StarterpackJoined -> UiTimeline.TopMessage.Icon.Info - ListNotificationsReason.Unverified -> UiTimeline.TopMessage.Icon.Info - ListNotificationsReason.Verified -> UiTimeline.TopMessage.Icon.Info - ListNotificationsReason.LikeViaRepost -> UiTimeline.TopMessage.Icon.Favourite - ListNotificationsReason.RepostViaRepost -> UiTimeline.TopMessage.Icon.Retweet - ListNotificationsReason.SubscribedPost -> UiTimeline.TopMessage.Icon.Info + ListNotificationsNotificationReason.Like -> UiTimeline.TopMessage.Icon.Favourite + ListNotificationsNotificationReason.Repost -> UiTimeline.TopMessage.Icon.Retweet + ListNotificationsNotificationReason.Follow -> UiTimeline.TopMessage.Icon.Follow + ListNotificationsNotificationReason.Mention -> UiTimeline.TopMessage.Icon.Mention + ListNotificationsNotificationReason.Reply -> UiTimeline.TopMessage.Icon.Reply + ListNotificationsNotificationReason.Quote -> UiTimeline.TopMessage.Icon.Reply + is ListNotificationsNotificationReason.Unknown -> UiTimeline.TopMessage.Icon.Info + ListNotificationsNotificationReason.StarterpackJoined -> UiTimeline.TopMessage.Icon.Info + ListNotificationsNotificationReason.Unverified -> UiTimeline.TopMessage.Icon.Info + ListNotificationsNotificationReason.Verified -> UiTimeline.TopMessage.Icon.Info + ListNotificationsNotificationReason.LikeViaRepost -> UiTimeline.TopMessage.Icon.Favourite + ListNotificationsNotificationReason.RepostViaRepost -> UiTimeline.TopMessage.Icon.Retweet + ListNotificationsNotificationReason.SubscribedPost -> UiTimeline.TopMessage.Icon.Info } -private val ListNotificationsReason.type: UiTimeline.TopMessage.MessageType +private val ListNotificationsNotificationReason.type: UiTimeline.TopMessage.MessageType get() = when (this) { - ListNotificationsReason.Like -> UiTimeline.TopMessage.MessageType.Bluesky.Like - ListNotificationsReason.Repost -> UiTimeline.TopMessage.MessageType.Bluesky.Repost - ListNotificationsReason.Follow -> UiTimeline.TopMessage.MessageType.Bluesky.Follow - ListNotificationsReason.Mention -> UiTimeline.TopMessage.MessageType.Bluesky.Mention - ListNotificationsReason.Reply -> UiTimeline.TopMessage.MessageType.Bluesky.Reply - ListNotificationsReason.Quote -> UiTimeline.TopMessage.MessageType.Bluesky.Quote - is ListNotificationsReason.Unknown -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown - ListNotificationsReason.StarterpackJoined -> UiTimeline.TopMessage.MessageType.Bluesky.StarterpackJoined - ListNotificationsReason.Unverified -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown - ListNotificationsReason.Verified -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown - ListNotificationsReason.LikeViaRepost -> UiTimeline.TopMessage.MessageType.Bluesky.Like - ListNotificationsReason.RepostViaRepost -> UiTimeline.TopMessage.MessageType.Bluesky.Repost - ListNotificationsReason.SubscribedPost -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown + ListNotificationsNotificationReason.Like -> UiTimeline.TopMessage.MessageType.Bluesky.Like + ListNotificationsNotificationReason.Repost -> UiTimeline.TopMessage.MessageType.Bluesky.Repost + ListNotificationsNotificationReason.Follow -> UiTimeline.TopMessage.MessageType.Bluesky.Follow + ListNotificationsNotificationReason.Mention -> UiTimeline.TopMessage.MessageType.Bluesky.Mention + ListNotificationsNotificationReason.Reply -> UiTimeline.TopMessage.MessageType.Bluesky.Reply + ListNotificationsNotificationReason.Quote -> UiTimeline.TopMessage.MessageType.Bluesky.Quote + is ListNotificationsNotificationReason.Unknown -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown + ListNotificationsNotificationReason.StarterpackJoined -> UiTimeline.TopMessage.MessageType.Bluesky.StarterpackJoined + ListNotificationsNotificationReason.Unverified -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown + ListNotificationsNotificationReason.Verified -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown + ListNotificationsNotificationReason.LikeViaRepost -> UiTimeline.TopMessage.MessageType.Bluesky.Like + ListNotificationsNotificationReason.RepostViaRepost -> UiTimeline.TopMessage.MessageType.Bluesky.Repost + ListNotificationsNotificationReason.SubscribedPost -> UiTimeline.TopMessage.MessageType.Bluesky.UnKnown } internal fun PostView.render(