Timeline : makes sure to use the right timeline when making some action (edit, reply, reaction)

This commit is contained in:
ganfra 2024-04-24 16:42:35 +02:00
parent b40f01a634
commit bb0ba5c4bf
7 changed files with 99 additions and 22 deletions

View file

@ -35,6 +35,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
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.TimelineEvents
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
@ -71,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val timelineController: TimelineController,
) : Node(buildContext, plugins = plugins), MessagesNavigator { ) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(this) private val presenter = presenterFactory.create(this)
private val callback = plugins<Callback>().firstOrNull() private val callback = plugins<Callback>().firstOrNull()
@ -101,6 +103,7 @@ class MessagesNode @AssistedInject constructor(
analyticsService.capture(room.toAnalyticsViewRoom()) analyticsService.capture(room.toAnalyticsViewRoom())
}, },
onDestroy = { onDestroy = {
timelineController.close()
mediaPlayer.close() mediaPlayer.close()
} }
) )

View file

@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
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.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.TimelineState
@ -116,6 +117,7 @@ class MessagesPresenter @AssistedInject constructor(
private val htmlConverterProvider: HtmlConverterProvider, private val htmlConverterProvider: HtmlConverterProvider,
@Assisted private val navigator: MessagesNavigator, @Assisted private val navigator: MessagesNavigator,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val timelineController: TimelineController,
) : Presenter<MessagesState> { ) : Presenter<MessagesState> {
private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator) private val timelinePresenter = timelinePresenterFactory.create(navigator = navigator)
@ -286,8 +288,10 @@ class MessagesPresenter @AssistedInject constructor(
emoji: String, emoji: String,
eventId: EventId, eventId: EventId,
) = launch(dispatchers.io) { ) = launch(dispatchers.io) {
room.toggleReaction(emoji, eventId) timelineController.invokeOnTimeline {
.onFailure { Timber.e(it) } toggleReaction(emoji, eventId)
.onFailure { Timber.e(it) }
}
} }
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) { private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<AsyncData<Unit>>) = launch(dispatchers.io) {

View file

@ -38,6 +38,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.mentions.MentionSuggestion import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor import io.element.android.features.messages.impl.mentions.MentionSuggestionsProcessor
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -100,6 +101,7 @@ class MessageComposerPresenter @Inject constructor(
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
private val permalinkBuilder: PermalinkBuilder, private val permalinkBuilder: PermalinkBuilder,
permissionsPresenterFactory: PermissionsPresenter.Factory, permissionsPresenterFactory: PermissionsPresenter.Factory,
private val timelineController: TimelineController,
) : Presenter<MessageComposerState> { ) : Presenter<MessageComposerState> {
private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA) private val cameraPermissionPresenter = permissionsPresenterFactory.create(Manifest.permission.CAMERA)
private var pendingEvent: MessageComposerEvents? = null private var pendingEvent: MessageComposerEvents? = null
@ -264,7 +266,9 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerMode.Quote -> null is MessageComposerMode.Quote -> null
}.let { relatedEventId -> }.let { relatedEventId ->
appCoroutineScope.launch { appCoroutineScope.launch {
room.enterSpecialMode(relatedEventId) timelineController.invokeOnTimeline {
enterSpecialMode(relatedEventId)
}
} }
} }
} }
@ -386,16 +390,17 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerMode.Edit -> { is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId val transactionId = capturedMode.transactionId
room.editMessage(eventId, transactionId, message.markdown, message.html, mentions) timelineController.invokeOnTimeline {
editMessage(eventId, transactionId, message.markdown, message.html, mentions)
}
} }
is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> room.replyMessage( is MessageComposerMode.Reply -> {
capturedMode.eventId, timelineController.invokeOnTimeline {
message.markdown, replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
message.html, }
mentions }
)
} }
analyticsService.capture( analyticsService.capture(
Composer( Composer(

View file

@ -17,11 +17,14 @@
package io.element.android.features.messages.impl.timeline package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
@ -30,23 +33,25 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.io.Closeable
import java.util.Optional import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
/** /**
* This controller is responsible of using the right timeline to display messages. * This controller is responsible of using the right timeline to display messages and make associated actions.
* It can be focused on the live timeline or on a detached timeline (focusing an unknown event). * It can be focused on the live timeline or on a detached timeline (focusing an unknown event).
* This controller will replace the [LiveTimelineProvider] in the DI.
*/ */
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
@ContributesBinding(RoomScope::class, boundType = TimelineProvider::class, replaces = [LiveTimelineProvider::class])
class TimelineController @Inject constructor( class TimelineController @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
) { ) : Closeable, TimelineProvider {
private val liveTimeline = MutableStateFlow(room.liveTimeline) private val liveTimeline = MutableStateFlow(room.liveTimeline)
private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty()) private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty())
@ -60,6 +65,12 @@ class TimelineController @Inject constructor(
return detachedTimeline.map { !it.isPresent } return detachedTimeline.map { !it.isPresent }
} }
suspend fun invokeOnTimeline(block: suspend (Timeline.() -> Any)) {
currentTimelineFlow().first().run {
block(this)
}
}
suspend fun focusOnEvent(eventId: EventId): Result<Unit> { suspend fun focusOnEvent(eventId: EventId): Result<Unit> {
return try { return try {
val newDetachedTimeline = room.timelineFocusedOnEvent(eventId) val newDetachedTimeline = room.timelineFocusedOnEvent(eventId)
@ -93,6 +104,10 @@ class TimelineController @Inject constructor(
} }
} }
override fun close() {
focusOnLive()
}
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> { suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
return currentTimelineFlow().first().paginate(direction) return currentTimelineFlow().first().paginate(direction)
.onSuccess { hasReachedEnd -> .onSuccess { hasReachedEnd ->
@ -124,7 +139,6 @@ class TimelineController @Inject constructor(
if (eventId != null && eventId != lastReadReceiptId.value) { if (eventId != null && eventId != lastReadReceiptId.value) {
lastReadReceiptId.value = eventId lastReadReceiptId.value = eventId
currentTimelineFlow() currentTimelineFlow()
.filterIsInstance(Timeline::class)
.first() .first()
.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) .sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
} }
@ -140,4 +154,8 @@ class TimelineController @Inject constructor(
} }
return null return null
} }
override suspend fun getActiveTimeline(): Timeline {
return currentTimelineFlow().first()
}
} }

View file

@ -20,15 +20,18 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import javax.inject.Inject import javax.inject.Inject
class PollRepository @Inject constructor( class PollRepository @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val timelineProvider: TimelineProvider,
) { ) {
suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatching { suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatching {
room.liveTimeline timelineProvider
.getActiveTimeline()
.timelineItems .timelineItems
.first() .first()
.asSequence() .asSequence()
@ -51,13 +54,15 @@ class PollRepository @Inject constructor(
maxSelections = maxSelections, maxSelections = maxSelections,
pollKind = pollKind, pollKind = pollKind,
) )
else -> room.editPoll( else -> timelineProvider
pollStartId = existingPollId, .getActiveTimeline()
question = question, .editPoll(
answers = answers, pollStartId = existingPollId,
maxSelections = maxSelections, question = question,
pollKind = pollKind, answers = answers,
) maxSelections = maxSelections,
pollKind = pollKind,
)
} }
suspend fun deletePoll( suspend fun deletePoll(

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.timeline
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.MatrixRoom
import javax.inject.Inject
/**
* This interface defines a way to get the active timeline.
* It could be the current room timeline, or a timeline for a specific event.
*/
interface TimelineProvider {
suspend fun getActiveTimeline(): Timeline
}
/**
* Default implementation of [TimelineProvider] that provides the live timeline of a room.
*/
@ContributesBinding(RoomScope::class)
class LiveTimelineProvider @Inject constructor(
private val room: MatrixRoom,
) : TimelineProvider {
override suspend fun getActiveTimeline(): Timeline = room.liveTimeline
}