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 220faf50d..fa1284ccd 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 @@ -570,102 +570,160 @@ internal suspend fun List.toDbPagingTimeline( } } }, -): List = - this.flatMap { - val reply = - when (val reply = it.reply?.parent) { - is ReplyRefParentUnion.PostView -> - if (reply.value.uri != it.post.uri) { - reply.value - } else { - null - } +): List { + // Build a map of all posts by URI for quick lookup + val postUriMap = mutableMapOf() + for (item in this) { + postUriMap[item.post.uri.atUri] = item + } + // Identify which posts are parents of other posts in this feed + val parentUrisInFeed = mutableSetOf() + for (item in this) { + val parentUri = + when (val parent = item.reply?.parent) { + is ReplyRefParentUnion.PostView -> parent.value.uri.atUri else -> null } + if (parentUri != null) { + val inFeed = postUriMap.containsKey(parentUri) + if (inFeed) { + parentUrisInFeed.add(parentUri) + } + } + } - val status = - when (val data = it.reason) { - is FeedViewPostReasonUnion.ReasonRepost -> { - val user = data.value.by.toDbUser(accountKey.host) - DbStatusWithUser( - user = user, - data = - DbStatus( - statusKey = - MicroBlogKey( - it.post.uri.atUri + "_reblog_${user.userKey}", - accountKey.host, - ), - userKey = - data.value.by - .toDbUser(accountKey.host) - .userKey, - content = StatusContent.BlueskyReason(data), - accountType = AccountType.Specific(accountKey), - text = null, - createdAt = it.post.indexedAt, - ), - ) + // Build complete parent chains for each post + val parentChainMap = mutableMapOf>() + for (item in this) { + val parentChain = mutableListOf() + var currentPostUri: String? = item.post.uri.atUri + val visitedUris = mutableSetOf() // prevent infinite loops + + while (currentPostUri != null && !visitedUris.contains(currentPostUri) && parentChain.size < 1000) { + visitedUris.add(currentPostUri) + val currentFeedItem = postUriMap[currentPostUri] + if (currentFeedItem != null) { + val parentPostView = + when (val reply = currentFeedItem.reply?.parent) { + is ReplyRefParentUnion.PostView -> reply.value + else -> null + } + if (parentPostView != null) { + parentChain.add(parentPostView) + currentPostUri = parentPostView.uri.atUri + } else { + currentPostUri = null } + } else { + currentPostUri = null + } + } - is FeedViewPostReasonUnion.ReasonPin -> { - val status = it.post.toDbStatusWithUser(accountKey) - DbStatusWithUser( - user = status.user, - data = - DbStatus( - statusKey = - MicroBlogKey( - it.post.uri.atUri + "_pin_${status.user?.userKey}", - accountKey.host, - ), - userKey = status.user?.userKey, - content = StatusContent.BlueskyReason(data), - accountType = AccountType.Specific(accountKey), - text = status.data.text, - createdAt = it.post.indexedAt, - ), - ) - } + if (parentChain.isNotEmpty()) { + parentChainMap[item.post.uri.atUri] = parentChain + } + } - else -> { - // bluesky doesn't have "quote" and "retweet" as the same as the other platforms - it.post.toDbStatusWithUser(accountKey) - } + // Filter out posts that are parents of other posts, keeping only leaves/endpoints + val result = + this.flatMap { item -> + if (parentUrisInFeed.contains(item.post.uri.atUri)) { + emptyList() + } else { + processBlueskyFeedItem(item, accountKey, pagingKey, sortIdProvider, parentChainMap) } - val references = - listOfNotNull( - if (reply != null) { - ReferenceType.Reply to listOfNotNull(reply.toDbStatusWithUser(accountKey)) - } else { - null - }, - if (it.reason != null) { - ReferenceType.Retweet to listOfNotNull(it.post.toDbStatusWithUser(accountKey)) + } + return result +} + +private suspend fun processBlueskyFeedItem( + it: FeedViewPost, + accountKey: MicroBlogKey, + pagingKey: String, + sortIdProvider: suspend (FeedViewPost) -> Long, + parentChainMap: Map>, +): List { + val parentChain = parentChainMap[it.post.uri.atUri].orEmpty() + + val status = + when (val data = it.reason) { + is FeedViewPostReasonUnion.ReasonRepost -> { + val user = data.value.by.toDbUser(accountKey.host) + DbStatusWithUser( + user = user, + data = + DbStatus( + statusKey = + MicroBlogKey( + it.post.uri.atUri + "_reblog_${user.userKey}", + accountKey.host, + ), + userKey = + data.value.by + .toDbUser(accountKey.host) + .userKey, + content = StatusContent.BlueskyReason(data), + accountType = AccountType.Specific(accountKey), + text = null, + createdAt = it.post.indexedAt, + ), + ) + } + + is FeedViewPostReasonUnion.ReasonPin -> { + val status = it.post.toDbStatusWithUser(accountKey) + DbStatusWithUser( + user = status.user, + data = + DbStatus( + statusKey = + MicroBlogKey( + it.post.uri.atUri + "_pin_${status.user?.userKey}", + accountKey.host, + ), + userKey = status.user?.userKey, + content = StatusContent.BlueskyReason(data), + accountType = AccountType.Specific(accountKey), + text = status.data.text, + createdAt = it.post.indexedAt, + ), + ) + } + + else -> { + // bluesky doesn't have "quote" and "retweet" as the same as the other platforms + it.post.toDbStatusWithUser(accountKey) + } + } + val references = + listOfNotNull( + if (parentChain.isNotEmpty()) { + val convertedParents = parentChain.mapNotNull { it.toDbStatusWithUser(accountKey) } + if (convertedParents.isNotEmpty()) { + ReferenceType.Reply to convertedParents } else { null - }, - ).toMap() - listOfNotNull( -// reply?.let { -// createDbPagingTimelineWithStatus( -// accountKey = accountKey, -// pagingKey = pagingKey, -// sortId = -SnowflakeIdGenerator.nextId(), -// status = it, -// references = references, -// ) -// }, - createDbPagingTimelineWithStatus( - accountKey = accountKey, - pagingKey = pagingKey, - sortId = sortIdProvider(it), - status = status, - references = references, - ), - ) - } + } + } else { + null + }, + if (it.reason != null) { + ReferenceType.Retweet to listOfNotNull(it.post.toDbStatusWithUser(accountKey)) + } else { + null + }, + ).toMap() + return listOfNotNull( + createDbPagingTimelineWithStatus( + accountKey = accountKey, + pagingKey = pagingKey, + sortId = sortIdProvider(it), + status = status, + references = references, + ), + ) +} private fun PostView.toDbStatusWithUser(accountKey: MicroBlogKey): DbStatusWithUser { val user = author.toDbUser(accountKey.host) 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 9f861406d..e4983b16b 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 @@ -529,6 +529,14 @@ internal fun PostView.renderStatus( append(uri.atUri.substringAfterLast("/")) } + val parents = + references[ReferenceType.Reply] + ?.mapNotNull { it as? StatusContent.Bluesky } + ?.reversed() + ?.map { it.data.renderStatus(accountKey, event) } + ?.toPersistentList() + ?: persistentListOf() + return UiTimeline.ItemContent.Status( platformType = PlatformType.Bluesky, user = user, @@ -539,10 +547,7 @@ internal fun PostView.renderStatus( poll = null, quote = listOfNotNull(findQuote(accountKey, this, event)).toImmutableList(), contentWarning = null, - parents = - listOfNotNull( - parent?.renderStatus(accountKey, event), - ).toPersistentList(), + parents = parents, actions = listOfNotNull( ActionMenu.Item(