From fad41409246eb15ce4c366ea9f7233cea7679425 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 10 Mar 2021 15:30:01 +0100 Subject: [PATCH] Message states: makes sure the actions bottom sheet is updated with synced event --- .../session/room/timeline/TimelineService.kt | 14 +++ .../internal/crypto/tasks/SendEventTask.kt | 8 +- .../room/timeline/DefaultTimelineService.kt | 10 +-- .../room/timeline/LiveTimelineEvent.kt | 90 +++++++++++++++++++ .../session/room/timeline/TimelineInput.kt | 11 ++- .../internal/session/sync/RoomSyncHandler.kt | 1 + .../timeline/action/MessageActionState.kt | 4 + .../action/MessageActionsEpoxyController.kt | 10 ++- .../action/MessageActionsViewModel.kt | 9 +- 9 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 2876ec69e5..3c021384e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -36,9 +36,23 @@ interface TimelineService { */ fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline + /** + * Returns a snapshot of TimelineEvent event with eventId. + * At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case. + * @param eventId the eventId to get the TimelineEvent + */ fun getTimeLineEvent(eventId: String): TimelineEvent? + /** + * Creates a LiveData of Optional TimelineEvent event with eventId. + * If the eventId is a local echo eventId, it will make the LiveData be updated with the synced TimelineEvent when coming through the sync. + * In this case, makes sure to use the new synced eventId from the TimelineEvent class if you want to interact, as the local echo is removed from the SDK. + * @param eventId the eventId to listen for TimelineEvent + */ fun getTimeLineEventLive(eventId: String): LiveData> + /** + * Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO. + */ fun getAttachmentMessages(): List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 1fbc30d6f6..04a4e3e52d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -15,7 +15,10 @@ */ package org.matrix.android.sdk.internal.crypto.tasks +import kotlinx.coroutines.delay +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest @@ -24,6 +27,7 @@ import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTa import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task +import java.lang.IllegalStateException import javax.inject.Inject internal interface SendEventTask : Task { @@ -51,7 +55,9 @@ internal class DefaultSendEventTask @Inject constructor( val event = handleEncryption(params) val localId = event.eventId!! - + if((event.content?.get("body") as? String)?.contains("Fail").orFalse()){ + throw IllegalStateException() + } localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING) val executeRequest = executeRequest(globalErrorReceiver) { apiCall = roomAPI.send( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index ef890db79e..9d003c1ff6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room.timeline import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory @@ -31,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper @@ -89,13 +87,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv } override fun getTimeLineEventLive(eventId: String): LiveData> { - val liveData = monarchy.findAllMappedWithChanges( - { TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) }, - { timelineEventMapper.map(it) } - ) - return Transformations.map(liveData) { events -> - events.firstOrNull().toOptional() - } + return LiveTimelineEvent(timelineInput, monarchy, taskExecutor, timelineEventMapper, roomId, eventId) } override fun getAttachmentMessages(): List { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt new file mode 100644 index 0000000000..0e9c917b05 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveTimelineEvent.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room.timeline + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Transformations +import com.zhuinden.monarchy.Monarchy +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.task.TaskExecutor + +/** + * This class takes care of handling case where local echo is replaced by the synced event in the db. + */ +internal class LiveTimelineEvent(private val timelineInput: TimelineInput, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val timelineEventMapper: TimelineEventMapper, + private val roomId: String, + private val eventId: String) + : TimelineInput.Listener, + MediatorLiveData>() { + + private var queryLiveData: LiveData>? = null + + init { + buildAndObserveQuery(eventId) + } + + // Makes sure it's made on the main thread + private fun buildAndObserveQuery(eventIdToObserve: String) = taskExecutor.executorScope.launch(Dispatchers.Main) { + queryLiveData?.also { + removeSource(it) + } + val liveData = monarchy.findAllMappedWithChanges( + { TimelineEventEntity.where(it, roomId = roomId, eventId = eventIdToObserve) }, + { timelineEventMapper.map(it) } + ) + queryLiveData = Transformations.map(liveData) { events -> + events.firstOrNull().toOptional() + } + queryLiveData?.also { + addSource(it) { newValue -> value = newValue } + } + } + + override fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) { + if (localEchoEventId == eventId) { + // rebuild the query with the new eventId + buildAndObserveQuery(syncedEventId) + } + } + + override fun onActive() { + super.onActive() + // If we are listening to local echo, we want to be aware when event is synced + if (LocalEcho.isLocalEchoId(eventId)) { + timelineInput.listeners.add(this) + } + } + + override fun onInactive() { + super.onInactive() + if (LocalEcho.isLocalEchoId(eventId)) { + timelineInput.listeners.remove(this) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt index 002ab1dd8a..8911f265d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt @@ -35,11 +35,16 @@ internal class TimelineInput @Inject constructor() { listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) } } + fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncEventId: String) { + listeners.toSet().forEach { it.onLocalEchoSynced(roomId, localEchoEventId, syncEventId) } + } + val listeners = mutableSetOf() internal interface Listener { - fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) - fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) - fun onNewTimelineEvents(roomId: String, eventIds: List) + fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit + fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit + fun onNewTimelineEvents(roomId: String, eventIds: List) = Unit + fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) = Unit } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index b2db6320f1..336a83eaad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -400,6 +400,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle event.mxDecryptionResult = adapter.fromJson(json) } } + timelineInput.onLocalEchoSynced(roomId, it, event.eventId) // Finally delete the local echo sendingEventEntity.deleteOnCascade(true) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt index 83d56a65c9..b11be0fe77 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized import im.vector.app.core.extensions.canReact import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent /** @@ -56,4 +57,7 @@ data class MessageActionState( fun senderName(): String = informationData.memberName?.toString() ?: "" fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact + + fun sendState(): SendState? = timelineEvent()?.root?.sendState + } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 57558efef1..fa92d5f136 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -34,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify +import org.matrix.android.sdk.api.extensions.orFalse import javax.inject.Inject /** @@ -63,7 +64,14 @@ class MessageActionsEpoxyController @Inject constructor( } // Send state - if (state.informationData.sendState.hasFailed()) { + val sendState = state.sendState() + if (sendState?.isSending().orFalse()) { + bottomSheetSendStateItem { + id("send_state") + showProgress(true) + text(stringProvider.getString(R.string.event_status_sending_message)) + } + } else if (sendState?.hasFailed().orFalse()) { bottomSheetSendStateItem { id("send_state") showProgress(false) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 363899c4f9..aac622e45e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -69,7 +69,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private val vectorPreferences: VectorPreferences ) : VectorViewModel(initialState) { - private val eventId = initialState.eventId private val informationData = initialState.informationData private val room = session.getRoom(initialState.roomId) private val pillsPostProcessor by lazy { @@ -91,7 +90,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted init { observeEvent() - observeReactions() observePowerLevel() observeTimelineEventState() } @@ -130,14 +128,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun observeEvent() { if (room == null) return room.rx() - .liveTimelineEvent(eventId) + .liveTimelineEvent(initialState.eventId) .unwrap() .execute { copy(timelineEvent = it) } } - private fun observeReactions() { + private fun observeReactions(eventId: String) { if (room == null) return room.rx() .liveAnnotationSummary(eventId) @@ -154,8 +152,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun observeTimelineEventState() { selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions -> val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe + observeReactions(nonNullTimelineEvent.eventId) setState { copy( + eventId = nonNullTimelineEvent.eventId, messageBody = computeMessageBody(nonNullTimelineEvent), actions = actionsForEvent(nonNullTimelineEvent, permissions) ) @@ -229,6 +229,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List { + val eventId = timelineEvent.eventId val messageContent = timelineEvent.getLastMessageContent() val msgType = messageContent?.msgType