Timeline : makes sure to use the right timeline when making some action (edit, reply, reaction)
This commit is contained in:
parent
b40f01a634
commit
bb0ba5c4bf
7 changed files with 99 additions and 22 deletions
|
|
@ -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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue