Message states: makes sure the actions bottom sheet is updated with synced event

This commit is contained in:
ganfra 2021-03-10 15:30:01 +01:00
parent fa40667633
commit fad4140924
9 changed files with 139 additions and 18 deletions

View file

@ -36,9 +36,23 @@ interface TimelineService {
*/ */
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline 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? 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<Optional<TimelineEvent>> fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
/**
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
*/
fun getAttachmentMessages(): List<TimelineEvent> fun getAttachmentMessages(): List<TimelineEvent>
} }

View file

@ -15,7 +15,10 @@
*/ */
package org.matrix.android.sdk.internal.crypto.tasks 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.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.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest 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.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import java.lang.IllegalStateException
import javax.inject.Inject import javax.inject.Inject
internal interface SendEventTask : Task<SendEventTask.Params, String> { internal interface SendEventTask : Task<SendEventTask.Params, String> {
@ -51,7 +55,9 @@ internal class DefaultSendEventTask @Inject constructor(
val event = handleEncryption(params) val event = handleEncryption(params)
val localId = event.eventId!! val localId = event.eventId!!
if((event.content?.get("body") as? String)?.contains("Fail").orFalse()){
throw IllegalStateException()
}
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING) localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(globalErrorReceiver) { val executeRequest = executeRequest<SendResponse>(globalErrorReceiver) {
apiCall = roomAPI.send( apiCall = roomAPI.send(

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory 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.TimelineService
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings 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.Optional
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider 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.ReadReceiptsSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper 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<Optional<TimelineEvent>> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
val liveData = monarchy.findAllMappedWithChanges( return LiveTimelineEvent(timelineInput, monarchy, taskExecutor, timelineEventMapper, roomId, eventId)
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
{ timelineEventMapper.map(it) }
)
return Transformations.map(liveData) { events ->
events.firstOrNull().toOptional()
}
} }
override fun getAttachmentMessages(): List<TimelineEvent> { override fun getAttachmentMessages(): List<TimelineEvent> {

View file

@ -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<Optional<TimelineEvent>>() {
private var queryLiveData: LiveData<Optional<TimelineEvent>>? = 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)
}
}
}

View file

@ -35,11 +35,16 @@ internal class TimelineInput @Inject constructor() {
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) } 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<Listener>() val listeners = mutableSetOf<Listener>()
internal interface Listener { internal interface Listener {
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit
fun onNewTimelineEvents(roomId: String, eventIds: List<String>) fun onNewTimelineEvents(roomId: String, eventIds: List<String>) = Unit
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) = Unit
} }
} }

View file

@ -400,6 +400,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
event.mxDecryptionResult = adapter.fromJson(json) event.mxDecryptionResult = adapter.fromJson(json)
} }
} }
timelineInput.onLocalEchoSynced(roomId, it, event.eventId)
// Finally delete the local echo // Finally delete the local echo
sendingEventEntity.deleteOnCascade(true) sendingEventEntity.deleteOnCascade(true)
} else { } else {

View file

@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.extensions.canReact import im.vector.app.core.extensions.canReact
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/** /**
@ -56,4 +57,7 @@ data class MessageActionState(
fun senderName(): String = informationData.memberName?.toString() ?: "" fun senderName(): String = informationData.memberName?.toString() ?: ""
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
fun sendState(): SendState? = timelineEvent()?.root?.sendState
} }

View file

@ -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.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.home.room.detail.timeline.tools.linkify
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -63,7 +64,14 @@ class MessageActionsEpoxyController @Inject constructor(
} }
// Send state // 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 { bottomSheetSendStateItem {
id("send_state") id("send_state")
showProgress(false) showProgress(false)

View file

@ -69,7 +69,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
private val eventId = initialState.eventId
private val informationData = initialState.informationData private val informationData = initialState.informationData
private val room = session.getRoom(initialState.roomId) private val room = session.getRoom(initialState.roomId)
private val pillsPostProcessor by lazy { private val pillsPostProcessor by lazy {
@ -91,7 +90,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
init { init {
observeEvent() observeEvent()
observeReactions()
observePowerLevel() observePowerLevel()
observeTimelineEventState() observeTimelineEventState()
} }
@ -130,14 +128,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeEvent() { private fun observeEvent() {
if (room == null) return if (room == null) return
room.rx() room.rx()
.liveTimelineEvent(eventId) .liveTimelineEvent(initialState.eventId)
.unwrap() .unwrap()
.execute { .execute {
copy(timelineEvent = it) copy(timelineEvent = it)
} }
} }
private fun observeReactions() { private fun observeReactions(eventId: String) {
if (room == null) return if (room == null) return
room.rx() room.rx()
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
@ -154,8 +152,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeTimelineEventState() { private fun observeTimelineEventState() {
selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions -> selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions ->
val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe
observeReactions(nonNullTimelineEvent.eventId)
setState { setState {
copy( copy(
eventId = nonNullTimelineEvent.eventId,
messageBody = computeMessageBody(nonNullTimelineEvent), messageBody = computeMessageBody(nonNullTimelineEvent),
actions = actionsForEvent(nonNullTimelineEvent, permissions) actions = actionsForEvent(nonNullTimelineEvent, permissions)
) )
@ -229,6 +229,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List<EventSharedAction> { private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List<EventSharedAction> {
val eventId = timelineEvent.eventId
val messageContent = timelineEvent.getLastMessageContent() val messageContent = timelineEvent.getLastMessageContent()
val msgType = messageContent?.msgType val msgType = messageContent?.msgType