diff --git a/shared/src/androidJvmMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.androidJvm.kt b/shared/src/androidJvmMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.androidJvm.kt index ef3bae2f3..dce4b5055 100644 --- a/shared/src/androidJvmMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.androidJvm.kt +++ b/shared/src/androidJvmMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.androidJvm.kt @@ -14,6 +14,8 @@ public actual data class UiDateTime internal constructor( internal actual fun Instant.toUi(): UiDateTime = UiDateTime(this) +internal actual operator fun UiDateTime.compareTo(other: UiDateTime): Int = value.compareTo(other.value) + public sealed interface LocalizedShortTime { public data class String( val value: kotlin.String, diff --git a/shared/src/appleMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.apple.kt b/shared/src/appleMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.apple.kt index d0674bdf5..18eb7c623 100644 --- a/shared/src/appleMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.apple.kt +++ b/shared/src/appleMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.apple.kt @@ -1,5 +1,6 @@ package dev.dimension.flare.ui.render +import kotlinx.datetime.toKotlinInstant import kotlinx.datetime.toNSDate import platform.Foundation.NSDate import kotlin.time.Instant @@ -7,3 +8,9 @@ import kotlin.time.Instant public actual typealias UiDateTime = NSDate internal actual fun Instant.toUi(): UiDateTime = toNSDate() + +internal actual operator fun UiDateTime.compareTo(other: UiDateTime): Int { + val instant = this.toKotlinInstant() + val otherInstant = other.toKotlinInstant() + return instant.compareTo(otherInstant) +} diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt index e0035b038..946cc7f84 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/data/datasource/bluesky/StatusDetailRemoteMediator.kt @@ -3,15 +3,19 @@ package dev.dimension.flare.data.datasource.bluesky import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState +import app.bsky.feed.FeedViewPost import app.bsky.feed.GetPostThreadQueryParams import app.bsky.feed.GetPostThreadResponseThreadUnion import app.bsky.feed.GetPostsQueryParams +import app.bsky.feed.ReplyRef +import app.bsky.feed.ReplyRefParentUnion +import app.bsky.feed.ReplyRefRootUnion import app.bsky.feed.ThreadViewPost import app.bsky.feed.ThreadViewPostParentUnion import app.bsky.feed.ThreadViewPostReplieUnion 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.database.cache.mapper.toDbPagingTimeline import dev.dimension.flare.data.database.cache.model.DbPagingTimeline import dev.dimension.flare.data.database.cache.model.DbPagingTimelineWithStatus import dev.dimension.flare.data.network.bluesky.BlueskyService @@ -78,7 +82,7 @@ internal class StatusDetailRemoteMediator( ).requireResponse() .posts .firstOrNull() - listOfNotNull(current) + listOfNotNull(current).map(::FeedViewPost) } else { val context = service @@ -102,11 +106,49 @@ internal class StatusDetailRemoteMediator( val replies = thread.value.replies.mapNotNull { when (it) { - is ThreadViewPostReplieUnion.ThreadViewPost -> it.value.post + is ThreadViewPostReplieUnion.ThreadViewPost -> { + if (it.value.replies.any()) { + val last = + it.value.replies.last().let { + when (it) { + is ThreadViewPostReplieUnion.ThreadViewPost -> it.value.post + else -> null + } + } + if (last != null) { + val parents = + listOfNotNull(it.value.post) + + it.value.replies.toList().dropLast(1).mapNotNull { + when (it) { + is ThreadViewPostReplieUnion.ThreadViewPost -> it.value.post + else -> null + } + } + val currentRef = + ReplyRef( + root = ReplyRefRootUnion.PostView(parents.last()), + parent = ReplyRefParentUnion.PostView(parents.last()), + ) + + FeedViewPost( + post = last, + reply = currentRef, + ) + } else { + FeedViewPost( + it.value.post, + ) + } + } else { + FeedViewPost( + it.value.post, + ) + } + } else -> null } } - parents.map { it.post }.reversed() + thread.value.post + replies + parents.map { FeedViewPost(it.post) }.reversed() + FeedViewPost(thread.value.post) + replies } else -> emptyList() @@ -115,7 +157,7 @@ internal class StatusDetailRemoteMediator( return Result( endOfPaginationReached = true, data = - result.toDb( + result.toDbPagingTimeline( accountKey, pagingKey, ) { diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt index 7bb5768f3..3adf4c0db 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/home/TimelinePresenter.kt @@ -132,15 +132,13 @@ public abstract class TimelinePresenter : }?.dataSource data .render(dataSource, useDbKeyInItemKey) - .let { - transform(it) - } + .let { transform(it) } } } } } - protected open fun transform(data: UiTimeline): UiTimeline = data + protected open suspend fun transform(data: UiTimeline): UiTimeline = data private fun networkPager( pagingSource: BaseTimelinePagingSourceFactory<*>, diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt index 8b34a13f0..0bb13cd79 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/presenter/status/StatusContextPresenter.kt @@ -8,12 +8,22 @@ import dev.dimension.flare.data.repository.accountServiceFlow import dev.dimension.flare.model.AccountType import dev.dimension.flare.model.MicroBlogKey import dev.dimension.flare.ui.model.UiTimeline +import dev.dimension.flare.ui.model.takeSuccess +import dev.dimension.flare.ui.model.toUi import dev.dimension.flare.ui.presenter.PresenterBase import dev.dimension.flare.ui.presenter.home.TimelinePresenter import dev.dimension.flare.ui.presenter.home.TimelineState +import dev.dimension.flare.ui.render.compareTo import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -23,35 +33,76 @@ public class StatusContextPresenter( ) : PresenterBase(), KoinComponent { private val accountRepository: AccountRepository by inject() + + @OptIn(ExperimentalCoroutinesApi::class) private val timelinePresenter by lazy { object : TimelinePresenter() { + private val currentStatusCreatedAtFlow by lazy { + accountServiceFlow( + accountType = accountType, + repository = accountRepository, + ).flatMapLatest { service -> + service.status(statusKey).toUi() + }.mapNotNull { it.takeSuccess() } + .mapNotNull { + if (it.content is UiTimeline.ItemContent.Status) { + it.content.createdAt + } else { + null + } + }.distinctUntilChanged() + } + override val loader: Flow by lazy { accountServiceFlow( accountType = accountType, repository = accountRepository, ).map { service -> service.context(statusKey) + }.combine(currentStatusCreatedAtFlow) { loader, _ -> + loader } } - override fun transform(data: UiTimeline): UiTimeline = - data.copy( + override suspend fun transform(data: UiTimeline): UiTimeline { + val currentCreatedAt = currentStatusCreatedAtFlow.firstOrNull() + return data.copy( content = when (val content = data.content) { - is UiTimeline.ItemContent.Status -> - content.copy( - parents = persistentListOf(), - ) + is UiTimeline.ItemContent.Status -> { + if (currentCreatedAt != null && content.createdAt <= currentCreatedAt) { + content.copy( + parents = persistentListOf(), + ) + } else if (currentCreatedAt != null) { + content.copy( + parents = + content.parents + .filter { + it.createdAt > currentCreatedAt + }.toPersistentList(), + ) + } else { + content + } + } + else -> content }, ) + } } } @Composable override fun body(): TimelineState { val listState = timelinePresenter.body() - remember { LogStatusHistoryPresenter(accountType = accountType, statusKey = statusKey) }.body() + remember { + LogStatusHistoryPresenter( + accountType = accountType, + statusKey = statusKey, + ) + }.body() return listState } } diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.kt index f87c3bba9..4d6828dd7 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/ui/render/UiDateTime.kt @@ -5,3 +5,5 @@ import kotlin.time.Instant public expect class UiDateTime internal expect fun Instant.toUi(): UiDateTime + +internal expect operator fun UiDateTime.compareTo(other: UiDateTime): Int