Fix marking a room as read re-instantiates its timeline (#5628)
* Add `Timeline.markAsRead` to avoid reinstantiating the timeline using `Room.markAsRead` * Mark as read when exiting the room screen, destroy the timeline when fully closed * Ensure `MarkAsFullyReadAndExit` event can only be processed once * Fix `DelayedVisibility` not being displayed in previews
This commit is contained in:
parent
bb61126c96
commit
6c3b280ecd
24 changed files with 281 additions and 89 deletions
|
|
@ -18,6 +18,7 @@ sealed interface MessagesEvents {
|
|||
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
|
||||
data class OnUserClicked(val user: MatrixUser) : MessagesEvents
|
||||
data object Dismiss : MessagesEvents
|
||||
data object MarkAsFullyReadAndExit : MessagesEvents
|
||||
}
|
||||
|
||||
enum class InviteDialogAction {
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ interface MessagesNavigator {
|
|||
fun onPreviewAttachment(attachments: ImmutableList<Attachment>, inReplyToEventId: EventId?)
|
||||
fun onNavigateToRoom(roomId: RoomId, eventId: EventId?, serverNames: List<String>)
|
||||
fun onOpenThread(threadRootId: ThreadId, focusedEventId: EventId?)
|
||||
fun onNavigateUp()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.messages.impl
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -263,6 +264,8 @@ class MessagesNode(
|
|||
context.toast(CommonStrings.screen_room_permalink_same_room_android)
|
||||
}
|
||||
|
||||
override fun onNavigateUp() = navigateUp()
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
|
|
@ -271,6 +274,11 @@ class MessagesNode(
|
|||
LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories,
|
||||
) {
|
||||
val state = presenter.present()
|
||||
|
||||
BackHandler {
|
||||
state.eventSink(MessagesEvents.MarkAsFullyReadAndExit)
|
||||
}
|
||||
|
||||
OnLifecycleEvent { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_PAUSE -> state.composerState.eventSink(MessageComposerEvents.SaveDraft)
|
||||
|
|
@ -279,7 +287,7 @@ class MessagesNode(
|
|||
}
|
||||
MessagesView(
|
||||
state = state,
|
||||
onBackClick = this::navigateUp,
|
||||
onBackClick = { state.eventSink(MessagesEvents.MarkAsFullyReadAndExit) },
|
||||
onRoomDetailsClick = this::onRoomDetailsClick,
|
||||
onEventContentClick = { isLive, event ->
|
||||
if (isLive) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.features.messages.impl.link.LinkState
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.features.messages.impl.timeline.MarkAsFullyRead
|
||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||
|
|
@ -65,6 +66,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.matrix.api.core.toThreadId
|
||||
|
|
@ -93,6 +95,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@AssistedInject
|
||||
class MessagesPresenter(
|
||||
|
|
@ -122,6 +125,8 @@ class MessagesPresenter(
|
|||
private val encryptionService: EncryptionService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val addRecentEmoji: AddRecentEmoji,
|
||||
private val markAsFullyRead: MarkAsFullyRead,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
) : Presenter<MessagesState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -138,10 +143,13 @@ class MessagesPresenter(
|
|||
timelineMode = timelineController.mainTimelineMode()
|
||||
)
|
||||
|
||||
private val markingAsReadAndExiting = AtomicBoolean(false)
|
||||
|
||||
@Composable
|
||||
override fun present(): MessagesState {
|
||||
htmlConverterProvider.Update()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val composerState = composerPresenter.present()
|
||||
|
|
@ -239,6 +247,22 @@ class MessagesPresenter(
|
|||
is MessagesEvents.OnUserClicked -> {
|
||||
roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user))
|
||||
}
|
||||
is MessagesEvents.MarkAsFullyReadAndExit -> coroutineScope.launch {
|
||||
if (!markingAsReadAndExiting.getAndSet(true)) {
|
||||
val latestEventId = room.liveTimeline.getLatestEventId().getOrElse {
|
||||
Timber.w(it, "Failed to get latest event id to mark as fully read")
|
||||
navigator.onNavigateUp()
|
||||
return@launch
|
||||
}
|
||||
latestEventId?.let { eventId ->
|
||||
sessionCoroutineScope.launch {
|
||||
markAsFullyRead(room.roomId, eventId)
|
||||
}
|
||||
}
|
||||
navigator.onNavigateUp()
|
||||
markingAsReadAndExiting.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ class ThreadedMessagesNode(
|
|||
callbacks.forEach { it.onOpenThread(threadRootId, focusedEventId) }
|
||||
}
|
||||
|
||||
override fun onNavigateUp() = navigateUp()
|
||||
|
||||
private fun onSendLocationClick() {
|
||||
callbacks.forEach { it.onSendLocationClick() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,29 +8,26 @@
|
|||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
interface MarkAsFullyRead {
|
||||
operator fun invoke(roomId: RoomId)
|
||||
suspend operator fun invoke(roomId: RoomId, eventId: EventId): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultMarkAsFullyRead(
|
||||
private val matrixClient: MatrixClient,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : MarkAsFullyRead {
|
||||
override fun invoke(roomId: RoomId) {
|
||||
matrixClient.sessionCoroutineScope.launch {
|
||||
matrixClient.getRoom(roomId)?.use { room ->
|
||||
room.markAsRead(receiptType = ReceiptType.FULLY_READ)
|
||||
.onFailure {
|
||||
Timber.e("Failed to mark room $roomId as fully read", it)
|
||||
}
|
||||
}
|
||||
override suspend fun invoke(roomId: RoomId, eventId: EventId): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
matrixClient.markRoomAsFullyRead(roomId, eventId).onFailure {
|
||||
Timber.e(it, "Failed to mark room $roomId as fully read for event $eventId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
|
@ -85,7 +84,6 @@ class TimelinePresenter(
|
|||
private val resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState>,
|
||||
private val typingNotificationPresenter: Presenter<TypingNotificationState>,
|
||||
private val roomCallStatePresenter: Presenter<RoomCallState>,
|
||||
private val markAsFullyRead: MarkAsFullyRead,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<TimelineState> {
|
||||
@AssistedFactory
|
||||
|
|
@ -219,12 +217,6 @@ class TimelinePresenter(
|
|||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
markAsFullyRead(room.roomId)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
timelineItemsFactory.timelineItems
|
||||
.onEach { newTimelineItems ->
|
||||
|
|
@ -388,7 +380,7 @@ class TimelinePresenter(
|
|||
) = launch(dispatchers.computation) {
|
||||
// If we are at the bottom of timeline, we mark the room as read.
|
||||
if (firstVisibleIndex == 0) {
|
||||
room.markAsRead(receiptType = readReceiptType)
|
||||
room.liveTimeline.markAsRead(receiptType = readReceiptType)
|
||||
} else {
|
||||
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue