Merge pull request #2937 from vector-im/feature/ons/message_states

Improve the status of send messages
This commit is contained in:
Benoit Marty 2021-03-10 21:51:48 +01:00 committed by GitHub
commit 8a1a90d1b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 898 additions and 345 deletions

View File

@ -66,11 +66,11 @@ interface RelationService {
/** /**
* Edit a text message body. Limited to "m.text" contentType * Edit a text message body. Limited to "m.text" contentType
* @param targetEventId The event to edit * @param targetEvent The event to edit
* @param newBodyText The edited body * @param newBodyText The edited body
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition * @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/ */
fun editTextMessage(targetEventId: String, fun editTextMessage(targetEvent: TimelineEvent,
msgType: String, msgType: String,
newBodyText: CharSequence, newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,

View File

@ -132,4 +132,9 @@ interface SendService {
* Resend all failed messages one by one (and keep order) * Resend all failed messages one by one (and keep order)
*/ */
fun resendAllFailedMessages() fun resendAllFailedMessages()
/**
* Cancel all failed messages
*/
fun cancelAllFailedMessages()
} }

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

@ -51,7 +51,6 @@ internal class DefaultSendEventTask @Inject constructor(
val event = handleEncryption(params) val event = handleEncryption(params)
val localId = event.eventId!! val localId = event.eventId!!
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,14 +17,13 @@ package org.matrix.android.sdk.internal.session.room.relation
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
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.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -47,6 +46,7 @@ import timber.log.Timber
internal class DefaultRelationService @AssistedInject constructor( internal class DefaultRelationService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
private val eventEditor: EventEditor,
private val eventSenderProcessor: EventSenderProcessor, private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
@ -112,32 +112,19 @@ internal class DefaultRelationService @AssistedInject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun editTextMessage(targetEventId: String, override fun editTextMessage(targetEvent: TimelineEvent,
msgType: String, msgType: String,
newBodyText: CharSequence, newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String): Cancelable { compatibilityBodyText: String): Cancelable {
val event = eventFactory return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newBodyAutoMarkdown, compatibilityBodyText)
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
.also { saveLocalEcho(it) }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} }
override fun editReply(replyToEdit: TimelineEvent, override fun editReply(replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent, originalTimelineEvent: TimelineEvent,
newBodyText: String, newBodyText: String,
compatibilityBodyText: String): Cancelable { compatibilityBodyText: String): Cancelable {
val event = eventFactory.createReplaceTextOfReply( return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, compatibilityBodyText)
roomId,
replyToEdit,
originalTimelineEvent,
newBodyText,
true,
MessageType.MSGTYPE_TEXT,
compatibilityBodyText
)
.also { saveLocalEcho(it) }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} }
override suspend fun fetchEditHistory(eventId: String): List<Event> { override suspend fun fetchEditHistory(eventId: String): List<Event> {

View File

@ -0,0 +1,104 @@
/*
* Copyright 2021 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.relation
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.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import timber.log.Timber
import javax.inject.Inject
internal class EventEditor @Inject constructor(private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val localEchoRepository: LocalEchoRepository) {
fun editTextMessage(targetEvent: TimelineEvent,
msgType: String,
newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String): Cancelable {
val roomId = targetEvent.roomId
if (targetEvent.root.sendState.hasFailed()) {
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
val editedEvent = eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown).copy(
eventId = targetEvent.eventId
)
updateFailedEchoWithEvent(roomId, targetEvent.eventId, editedEvent)
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory
.createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
.also { localEchoRepository.createLocalEcho(it) }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} else {
// Should we throw?
Timber.w("Can't edit a sending event")
return NoOpCancellable
}
}
fun editReply(replyToEdit: TimelineEvent,
originalTimelineEvent: TimelineEvent,
newBodyText: String,
compatibilityBodyText: String): Cancelable {
val roomId = replyToEdit.roomId
if (replyToEdit.root.sendState.hasFailed()) {
// We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event.
val editedEvent = eventFactory.createReplyTextEvent(roomId, originalTimelineEvent, newBodyText, false)?.copy(
eventId = replyToEdit.eventId
) ?: return NoOpCancellable
updateFailedEchoWithEvent(roomId, replyToEdit.eventId, editedEvent)
return eventSenderProcessor.postEvent(editedEvent, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} else if (replyToEdit.root.sendState.isSent()) {
val event = eventFactory.createReplaceTextOfReply(
roomId,
replyToEdit,
originalTimelineEvent,
newBodyText,
true,
MessageType.MSGTYPE_TEXT,
compatibilityBodyText
)
.also { localEchoRepository.createLocalEcho(it) }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} else {
// Should we throw?
Timber.w("Can't edit a sending event")
return NoOpCancellable
}
}
private fun updateFailedEchoWithEvent(roomId: String, failedEchoEventId: String, editedEvent: Event) {
val editedEventEntity = editedEvent.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis())
localEchoRepository.updateEchoAsync(failedEchoEventId) { _, entity ->
entity.content = editedEventEntity.content
entity.ageLocalTs = editedEventEntity.ageLocalTs
entity.age = editedEventEntity.age
entity.originServerTs = editedEventEntity.originServerTs
entity.sendState = editedEventEntity.sendState
}
}
}

View File

@ -232,6 +232,14 @@ internal class DefaultSendService @AssistedInject constructor(
} }
} }
override fun cancelAllFailedMessages() {
taskExecutor.executorScope.launch {
localEchoRepository.getAllFailedEventsToResend(roomId).forEach { event ->
cancelSend(event.eventId)
}
}
}
override fun sendMedia(attachment: ContentAttachmentData, override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean, compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable { roomIds: Set<String>): Cancelable {

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.executorScope, 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,94 @@
/*
* 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.CoroutineScope
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 java.util.concurrent.atomic.AtomicBoolean
/**
* 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 coroutineScope: CoroutineScope,
private val timelineEventMapper: TimelineEventMapper,
private val roomId: String,
private val eventId: String)
: TimelineInput.Listener,
MediatorLiveData<Optional<TimelineEvent>>() {
private var queryLiveData: LiveData<Optional<TimelineEvent>>? = null
// If we are listening to local echo, we want to be aware when event is synced
private var shouldObserveSync = AtomicBoolean(LocalEcho.isLocalEchoId(eventId))
init {
buildAndObserveQuery(eventId)
}
// Makes sure it's made on the main thread
private fun buildAndObserveQuery(eventIdToObserve: String) = coroutineScope.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()
}.also {
addSource(it) { newValue -> value = newValue }
}
}
override fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) {
if (this.roomId == roomId && localEchoEventId == this.eventId) {
timelineInput.listeners.remove(this)
shouldObserveSync.set(false)
// rebuild the query with the new eventId
buildAndObserveQuery(syncedEventId)
}
}
override fun onActive() {
super.onActive()
if (shouldObserveSync.get()) {
timelineInput.listeners.add(this)
}
}
override fun onInactive() {
super.onInactive()
if (shouldObserveSync.get()) {
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

@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===91 enum class===92
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2021 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 im.vector.app.core.ui.views
import android.content.Context
import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.databinding.ViewFailedMessagesWarningBinding
class FailedMessagesWarningView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onDeleteAllClicked()
fun onRetryClicked()
}
var callback: Callback? = null
private lateinit var views: ViewFailedMessagesWarningBinding
init {
setupViews()
}
private fun setupViews() {
inflate(context, R.layout.view_failed_messages_warning, this)
views = ViewFailedMessagesWarningBinding.bind(this)
views.failedMessagesDeleteAllButton.setOnClickListener { callback?.onDeleteAllClicked() }
views.failedMessagesRetryButton.setOnClickListener { callback?.onRetryClicked() }
}
fun render(hasFailedMessages: Boolean) {
isVisible = hasFailedMessages
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2021 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 im.vector.app.core.ui.views
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
class SendStateImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {
init {
if (isInEditMode) {
render(SendStateDecoration.SENT)
}
}
fun render(sendState: SendStateDecoration) {
isVisible = when (sendState) {
SendStateDecoration.SENDING_NON_MEDIA -> {
setImageResource(R.drawable.ic_sending_message)
contentDescription = context.getString(R.string.event_status_a11y_sending)
true
}
SendStateDecoration.SENT -> {
setImageResource(R.drawable.ic_message_sent)
contentDescription = context.getString(R.string.event_status_a11y_sent)
true
}
SendStateDecoration.FAILED -> {
setImageResource(R.drawable.ic_sending_message_failed)
contentDescription = context.getString(R.string.event_status_a11y_failed)
true
}
SendStateDecoration.SENDING_MEDIA,
SendStateDecoration.NONE -> {
false
}
}
}
}

View File

@ -106,4 +106,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction() data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction() data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction()
// Failed messages
object RemoveAllFailedMessages : RoomDetailAction()
} }

View File

@ -94,6 +94,7 @@ import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.FailedMessagesWarningView
import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.JumpToReadMarkerView
import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.Debouncer
@ -325,6 +326,7 @@ class RoomDetailFragment @Inject constructor(
setupJumpToBottomView() setupJumpToBottomView()
setupConfBannerView() setupConfBannerView()
setupEmojiPopup() setupEmojiPopup()
setupFailedMessagesWarningView()
views.roomToolbarContentView.debouncedClicks { views.roomToolbarContentView.debouncedClicks {
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
@ -557,6 +559,25 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun setupFailedMessagesWarningView() {
views.failedMessagesWarningView.callback = object : FailedMessagesWarningView.Callback {
override fun onDeleteAllClicked() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.event_status_delete_all_failed_dialog_title)
.setMessage(getString(R.string.event_status_delete_all_failed_dialog_message))
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.RemoveAllFailedMessages)
}
.show()
}
override fun onRetryClicked() {
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
}
}
}
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) { private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo)) navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
} }
@ -776,10 +797,6 @@ class RoomDetailFragment @Inject constructor(
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
true true
} }
R.id.resend_all -> {
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
true
}
R.id.open_matrix_apps -> { R.id.open_matrix_apps -> {
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
true true
@ -1171,6 +1188,7 @@ class RoomDetailFragment @Inject constructor(
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
renderToolbar(summary, state.typingMessage) renderToolbar(summary, state.typingMessage)
views.activeConferenceView.render(state) views.activeConferenceView.render(state)
views.failedMessagesWarningView.render(state.hasFailedSending)
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.count = summary.notificationCount
@ -1547,9 +1565,21 @@ class RoomDetailFragment @Inject constructor(
MessageActionsBottomSheet MessageActionsBottomSheet
.newInstance(roomId, informationData) .newInstance(roomId, informationData)
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
return true return true
} }
private fun handleCancelSend(action: EventSharedAction.Cancel) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation)
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { _, _ ->
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId))
}
.show()
}
override fun onAvatarClicked(informationData: MessageInformationData) { override fun onAvatarClicked(informationData: MessageInformationData) {
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId)) // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
openRoomMemberProfile(informationData.senderId) openRoomMemberProfile(informationData.senderId)
@ -1745,7 +1775,7 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
} }
is EventSharedAction.Cancel -> { is EventSharedAction.Cancel -> {
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId)) handleCancelSend(action)
} }
is EventSharedAction.ReportContentSpam -> { is EventSharedAction.ReportContentSpam -> {
roomDetailViewModel.handle(RoomDetailAction.ReportContent( roomDetailViewModel.handle(RoomDetailAction.ReportContent(

View File

@ -262,66 +262,68 @@ class RoomDetailViewModel @AssistedInject constructor(
override fun handle(action: RoomDetailAction) { override fun handle(action: RoomDetailAction) {
when (action) { when (action) {
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action) is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
is RoomDetailAction.SaveDraft -> handleSaveDraft(action) is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
is RoomDetailAction.SendMessage -> handleSendMessage(action) is RoomDetailAction.SendMessage -> handleSendMessage(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action) is RoomDetailAction.SendMedia -> handleSendMedia(action)
is RoomDetailAction.SendSticker -> handleSendSticker(action) is RoomDetailAction.SendSticker -> handleSendSticker(action)
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailAction.SendReaction -> handleSendReaction(action) is RoomDetailAction.SendReaction -> handleSendReaction(action)
is RoomDetailAction.AcceptInvite -> handleAcceptInvite() is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
is RoomDetailAction.RejectInvite -> handleRejectInvite() is RoomDetailAction.RejectInvite -> handleRejectInvite()
is RoomDetailAction.RedactAction -> handleRedactEvent(action) is RoomDetailAction.RedactAction -> handleRedactEvent(action)
is RoomDetailAction.UndoReaction -> handleUndoReact(action) is RoomDetailAction.UndoReaction -> handleUndoReact(action)
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
is RoomDetailAction.EnterEditMode -> handleEditAction(action) is RoomDetailAction.EnterEditMode -> handleEditAction(action)
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action) is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.ResendAll -> handleResendAll() is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action) is RoomDetailAction.ReportContent -> handleReportContent(action)
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> { is RoomDetailAction.ShowRoomAvatarFullScreen -> {
_viewEvents.post( _viewEvents.post(
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
) )
} }
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
RoomDetailAction.ResendAll -> handleResendAll()
}.exhaustive }.exhaustive
} }
@ -660,10 +662,8 @@ class RoomDetailViewModel @AssistedInject constructor(
return@withState false return@withState false
} }
when (itemId) { when (itemId) {
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true R.id.timeline_setting -> true
R.id.invite -> state.canInvite R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call,
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
@ -816,7 +816,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
}.exhaustive }.exhaustive
} }
is SendMode.EDIT -> { is SendMode.EDIT -> {
// is original event a reply? // is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
if (inReplyTo != null) { if (inReplyTo != null) {
@ -828,7 +828,7 @@ class RoomDetailViewModel @AssistedInject constructor(
val messageContent = state.sendMode.timelineEvent.getLastMessageContent() val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val existingBody = messageContent?.body ?: "" val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) { if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", room.editTextMessage(state.sendMode.timelineEvent,
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT, messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
action.text, action.text,
action.autoMarkdown) action.autoMarkdown)
@ -839,7 +839,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.QUOTE -> { is SendMode.QUOTE -> {
val messageContent = state.sendMode.timelineEvent.getLastMessageContent() val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
val textMsg = messageContent?.body val textMsg = messageContent?.body
@ -860,7 +860,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft() popDraft()
} }
is SendMode.REPLY -> { is SendMode.REPLY -> {
state.sendMode.timelineEvent.let { state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown) room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(RoomDetailViewEvents.MessageSent) _viewEvents.post(RoomDetailViewEvents.MessageSent)
@ -1223,6 +1223,10 @@ class RoomDetailViewModel @AssistedInject constructor(
room.resendAllFailedMessages() room.resendAllFailedMessages()
} }
private fun handleRemoveAllFailedMessages() {
room.cancelAllFailedMessages()
}
private fun observeEventDisplayedActions() { private fun observeEventDisplayedActions() {
// We are buffering scroll events for one second // We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on. // and keep the most recent one to set the read receipt on.
@ -1437,7 +1441,10 @@ class RoomDetailViewModel @AssistedInject constructor(
roomSummariesHolder.set(summary) roomSummariesHolder.set(summary)
setState { setState {
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
copy(typingMessage = typingMessage) copy(
typingMessage = typingMessage,
hasFailedSending = summary.hasFailedSending
)
} }
if (summary.membership == Membership.INVITE) { if (summary.membership == Membership.INVITE) {
summary.inviterId?.let { inviterId -> summary.inviterId?.let { inviterId ->

View File

@ -75,7 +75,8 @@ data class RoomDetailViewState(
val canInvite: Boolean = true, val canInvite: Boolean = true,
val isAllowedToManageWidgets: Boolean = false, val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val showDialerOption: Boolean = false val showDialerOption: Boolean = false,
val hasFailedSending: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this( constructor(args: RoomDetailArgs) : this(

View File

@ -50,17 +50,8 @@ class MessageColorProvider @Inject constructor(
SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color) SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color)
} }
} else { } else {
// When not in developer mode, we do not use special color for the encrypting state // When not in developer mode, we use only one color
when (sendState) { colorProvider.getColorFromAttribute(R.attr.vctr_message_text_color)
SendState.UNKNOWN,
SendState.UNSENT,
SendState.ENCRYPTING,
SendState.SENDING -> colorProvider.getColorFromAttribute(R.attr.vctr_sending_message_text_color)
SendState.SENT,
SendState.SYNCED -> colorProvider.getColorFromAttribute(R.attr.vctr_message_text_color)
SendState.UNDELIVERED,
SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color)
}
} }
} }
} }

View File

@ -30,6 +30,7 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.RoomDetailViewState
@ -42,11 +43,13 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineControlle
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
@ -336,11 +339,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData { private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition] val event = items[currentPosition]
val nextEvent = items.nextOrNull(currentPosition) val nextEvent = items.nextOrNull(currentPosition)
val prevEvent = items.prevOrNull(currentPosition)
if (hasReachedInvite && hasUTD) { if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId, null, null, null) return CacheItemData(event.localId, event.root.eventId, null, null, null)
} }
updateUTDStates(event, nextEvent) updateUTDStates(event, nextEvent)
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also { val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also {
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
} }
@ -362,7 +366,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
requestModelBuild() requestModelBuild()
} }
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs) val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) // If we have a SENT decoration, we want to built again as it might have to be changed to NONE if more recent event has also SENT decoration
val forceTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, forceTriggerBuild)
} }
private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? { private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
@ -425,11 +431,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val eventId: String?, val eventId: String?,
val eventModel: EpoxyModel<*>? = null, val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null val formattedDayModel: DaySeparatorItem? = null,
val forceTriggerBuild: Boolean = false
) { ) {
fun shouldTriggerBuild(): Boolean { fun shouldTriggerBuild(): Boolean {
// Since those items can change when we paginate, force a re-build // Since those items can change when we paginate, force a re-build
return mergedHeaderModel != null || formattedDayModel != null return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
} }
} }
} }

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,6 @@ 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,8 @@ 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 org.matrix.android.sdk.api.session.room.send.SendState
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -63,23 +65,24 @@ class MessageActionsEpoxyController @Inject constructor(
} }
// Send state // Send state
if (state.informationData.sendState.isSending()) { val sendState = state.sendState()
bottomSheetSendStateItem { if (sendState?.hasFailed().orFalse()) {
id("send_state")
showProgress(true)
text(stringProvider.getString(R.string.event_status_sending_message))
}
} else if (state.informationData.sendState.hasFailed()) {
bottomSheetSendStateItem { bottomSheetSendStateItem {
id("send_state") id("send_state")
showProgress(false) showProgress(false)
text(stringProvider.getString(R.string.unable_to_send_message)) text(stringProvider.getString(R.string.unable_to_send_message))
drawableStart(R.drawable.ic_warning_badge) drawableStart(R.drawable.ic_warning_badge)
} }
} else if (sendState != SendState.SYNCED) {
bottomSheetSendStateItem {
id("send_state")
showProgress(true)
text(stringProvider.getString(R.string.event_status_sending_message))
}
} }
when (state.informationData.e2eDecoration) { when (state.informationData.e2eDecoration) {
E2EDecoration.WARN_IN_CLEAR -> { E2EDecoration.WARN_IN_CLEAR -> {
bottomSheetSendStateItem { bottomSheetSendStateItem {
id("e2e_clear") id("e2e_clear")
showProgress(false) showProgress(false)

View File

@ -18,10 +18,11 @@ package im.vector.app.features.home.room.detail.timeline.action
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import com.jakewharton.rxrelay2.BehaviorRelay
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.Lazy import dagger.Lazy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.canReact import im.vector.app.core.extensions.canReact
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
@ -69,13 +70,14 @@ 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 {
pillsPostProcessorFactory.create(initialState.roomId) pillsPostProcessorFactory.create(initialState.roomId)
} }
private val eventIdObservable = BehaviorRelay.createDefault(initialState.eventId)
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel fun create(initialState: MessageActionState): MessageActionsViewModel
@ -130,7 +132,7 @@ 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)
@ -139,12 +141,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeReactions() { private fun observeReactions() {
if (room == null) return if (room == null) return
room.rx() eventIdObservable
.liveAnnotationSummary(eventId) .switchMap { eventId ->
.map { annotations -> room.rx()
EmojiDataSource.quickEmojis.map { emoji -> .liveAnnotationSummary(eventId)
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) .map { annotations ->
} EmojiDataSource.quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
}
}
} }
.execute { .execute {
copy(quickStates = it) copy(quickStates = it)
@ -154,8 +159,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
eventIdObservable.accept(nonNullTimelineEvent.eventId)
setState { setState {
copy( copy(
eventId = nonNullTimelineEvent.eventId,
messageBody = computeMessageBody(nonNullTimelineEvent), messageBody = computeMessageBody(nonNullTimelineEvent),
actions = actionsForEvent(nonNullTimelineEvent, permissions) actions = actionsForEvent(nonNullTimelineEvent, permissions)
) )
@ -233,94 +240,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
val msgType = messageContent?.msgType val msgType = messageContent?.msgType
return arrayListOf<EventSharedAction>().apply { return arrayListOf<EventSharedAction>().apply {
if (timelineEvent.root.sendState.hasFailed()) { when {
if (canRetry(timelineEvent, actionPermissions)) { timelineEvent.root.sendState.hasFailed() -> {
add(EventSharedAction.Resend(eventId)) addActionsForFailedState(timelineEvent, actionPermissions, messageContent, msgType)
} }
add(EventSharedAction.Remove(eventId)) timelineEvent.root.sendState.isSending() -> {
if (vectorPreferences.developerMode()) { addActionsForSendingState(timelineEvent)
addViewSourceItems(timelineEvent)
} }
} else if (timelineEvent.root.sendState.isSending()) { timelineEvent.root.sendState == SendState.SYNCED -> {
// TODO is uploading attachment? addActionsForSyncedState(timelineEvent, actionPermissions, messageContent, msgType)
if (canCancel(timelineEvent)) {
add(EventSharedAction.Cancel(eventId))
}
} else if (timelineEvent.root.sendState == SendState.SYNCED) {
if (!timelineEvent.root.isRedacted()) {
if (canReply(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Reply(eventId))
}
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
add(EventSharedAction.Edit(eventId))
}
if (canRedact(timelineEvent, actionPermissions)) {
add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId))
}
if (canCopy(msgType)) {
// TODO copy images? html? see ClipBoard
add(EventSharedAction.Copy(messageContent!!.body))
}
if (timelineEvent.canReact() && actionPermissions.canReact) {
add(EventSharedAction.AddReaction(eventId))
}
if (canQuote(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Quote(eventId))
}
if (canViewReactions(timelineEvent)) {
add(EventSharedAction.ViewReactions(informationData))
}
if (timelineEvent.hasBeenEdited()) {
add(EventSharedAction.ViewEditHistory(informationData))
}
if (canShare(msgType)) {
add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!))
}
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
}
if (timelineEvent.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
}
if (vectorPreferences.developerMode()) {
if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) {
val keysBackupService = session.cryptoService().keysBackupService()
if (keysBackupService.state == KeysBackupState.NotTrusted
|| (keysBackupService.state == KeysBackupState.ReadyToBackUp
&& keysBackupService.canRestoreKeys())
) {
add(EventSharedAction.UseKeyBackup)
}
if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1
|| timelineEvent.senderInfo.userId != session.myUserId) {
add(EventSharedAction.ReRequestKey(timelineEvent.eventId))
}
}
addViewSourceItems(timelineEvent)
}
add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != timelineEvent.root.senderId) {
// not sent by me
if (timelineEvent.root.getClearType() == EventType.MESSAGE) {
add(EventSharedAction.ReportContent(eventId, timelineEvent.root.senderId))
}
add(EventSharedAction.Separator)
add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
} }
} }
} }
@ -335,6 +263,116 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
} }
private fun ArrayList<EventSharedAction>.addActionsForFailedState(timelineEvent: TimelineEvent,
actionPermissions: ActionPermissions,
messageContent: MessageContent?,
msgType: String?) {
val eventId = timelineEvent.eventId
if (canRetry(timelineEvent, actionPermissions)) {
add(EventSharedAction.Resend(eventId))
}
add(EventSharedAction.Remove(eventId))
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
add(EventSharedAction.Edit(eventId))
}
if (canCopy(msgType)) {
// TODO copy images? html? see ClipBoard
add(EventSharedAction.Copy(messageContent!!.body))
}
if (vectorPreferences.developerMode()) {
addViewSourceItems(timelineEvent)
}
}
private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) {
// TODO is uploading attachment?
if (canCancel(timelineEvent)) {
add(EventSharedAction.Cancel(timelineEvent.eventId))
}
}
private fun ArrayList<EventSharedAction>.addActionsForSyncedState(timelineEvent: TimelineEvent,
actionPermissions: ActionPermissions,
messageContent: MessageContent?,
msgType: String?) {
val eventId = timelineEvent.eventId
if (!timelineEvent.root.isRedacted()) {
if (canReply(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Reply(eventId))
}
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
add(EventSharedAction.Edit(eventId))
}
if (canRedact(timelineEvent, actionPermissions)) {
add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId))
}
if (canCopy(msgType)) {
// TODO copy images? html? see ClipBoard
add(EventSharedAction.Copy(messageContent!!.body))
}
if (timelineEvent.canReact() && actionPermissions.canReact) {
add(EventSharedAction.AddReaction(eventId))
}
if (canQuote(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Quote(eventId))
}
if (canViewReactions(timelineEvent)) {
add(EventSharedAction.ViewReactions(informationData))
}
if (timelineEvent.hasBeenEdited()) {
add(EventSharedAction.ViewEditHistory(informationData))
}
if (canShare(msgType)) {
add(EventSharedAction.Share(timelineEvent.eventId, messageContent!!))
}
if (canSave(msgType) && messageContent is MessageWithAttachmentContent) {
add(EventSharedAction.Save(timelineEvent.eventId, messageContent))
}
if (timelineEvent.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
}
if (vectorPreferences.developerMode()) {
if (timelineEvent.isEncrypted() && timelineEvent.root.mCryptoError != null) {
val keysBackupService = session.cryptoService().keysBackupService()
if (keysBackupService.state == KeysBackupState.NotTrusted
|| (keysBackupService.state == KeysBackupState.ReadyToBackUp
&& keysBackupService.canRestoreKeys())
) {
add(EventSharedAction.UseKeyBackup)
}
if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1
|| timelineEvent.senderInfo.userId != session.myUserId) {
add(EventSharedAction.ReRequestKey(timelineEvent.eventId))
}
}
addViewSourceItems(timelineEvent)
}
add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != timelineEvent.root.senderId) {
// not sent by me
if (timelineEvent.root.getClearType() == EventType.MESSAGE) {
add(EventSharedAction.ReportContent(eventId, timelineEvent.root.senderId))
}
add(EventSharedAction.Separator)
add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
}
}
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean { private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
return true return true
} }

View File

@ -52,7 +52,7 @@ class CallItemFactory @Inject constructor(
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
if (event.root.eventId == null) return null if (event.root.eventId == null) return null
val roomId = event.roomId val roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, null) val informationData = messageInformationDataFactory.create(event, null, null)
val callSignalingContent = event.getCallSignallingContent() ?: return null val callSignalingContent = event.getCallSignallingContent() ?: return null
val callId = callSignalingContent.callId ?: return null val callId = callSignalingContent.callId ?: return null
val call = callManager.getCallById(callId) val call = callManager.getCallById(callId)

View File

@ -61,7 +61,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
} else { } else {
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId) stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
} }
val informationData = informationDataFactory.create(event, null) val informationData = informationDataFactory.create(event, null, null)
return create(text, informationData, highlight, callback) return create(text, informationData, highlight, callback)
} }
} }

View File

@ -47,6 +47,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
@ -108,7 +109,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
} }
} }
val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback) val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
return MessageTextItem_() return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)

View File

@ -48,7 +48,7 @@ class EncryptionItemFactory @Inject constructor(
return null return null
} }
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(event, null) val informationData = informationDataFactory.create(event, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM

View File

@ -119,13 +119,14 @@ class MessageItemFactory @Inject constructor(
} }
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
roomId = event.roomId roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// message is redacted // message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)

View File

@ -35,7 +35,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? { callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null val formattedText = eventFormatter.format(event) ?: return null
val informationData = informationDataFactory.create(event, null) val informationData = informationDataFactory.create(event, null, null)
val attributes = NoticeItem.Attributes( val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,
informationData = informationData, informationData = informationData,

View File

@ -37,7 +37,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val callItemFactory: CallItemFactory, private val callItemFactory: CallItemFactory,
private val userPreferencesProvider: UserPreferencesProvider) { private val userPreferencesProvider: UserPreferencesProvider) {
/**
* Reminder: nextEvent is older and prevEvent is newer.
*/
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
eventIdToHighlight: String?, eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
@ -46,7 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
val computedModel = try { val computedModel = try {
when (event.root.getClearType()) { when (event.root.getClearType()) {
EventType.STICKER, EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
// State and call // State and call
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
@ -76,9 +80,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it // Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(event, nextEvent, highlight, callback) messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
} else { } else {
encryptedItemFactory.create(event, nextEvent, highlight, callback) encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
} }
} }
EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_ALIASES,

View File

@ -75,9 +75,9 @@ class VerificationItemFactory @Inject constructor(
// If it's not a request ignore this event // If it's not a request ignore this event
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback) // if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
val referenceInformationData = messageInformationDataFactory.create(refEvent, null) val referenceInformationData = messageInformationDataFactory.create(refEvent, null, null)
val informationData = messageInformationDataFactory.create(event, null) val informationData = messageInformationDataFactory.create(event, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)
when (event.root.getClearType()) { when (event.root.getClearType()) {

View File

@ -64,7 +64,7 @@ class WidgetItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
widgetContent: WidgetContent, widgetContent: WidgetContent,
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
val informationData = informationDataFactory.create(timelineEvent, null) val informationData = informationDataFactory.create(timelineEvent, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName

View File

@ -25,11 +25,13 @@ import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
@ -49,7 +51,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData {
// Non nullability has been tested before // Non nullability has been tested before
val eventId = event.root.eventId!! val eventId = event.root.eventId!!
@ -70,6 +72,19 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
val e2eDecoration = getE2EDecoration(event) val e2eDecoration = getE2EDecoration(event)
// SendState Decoration
val isSentByMe = event.root.senderId == session.myUserId
val sendStateDecoration = if (isSentByMe) {
getSendStateDecoration(
eventSendState = event.root.sendState,
prevEventSendState = prevEvent?.root?.sendState,
anyReadReceipts = event.readReceipts.any { it.user.userId != session.myUserId },
isMedia = event.root.isAttachmentMessage()
)
} else {
SendStateDecoration.NONE
}
return MessageInformationData( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = event.root.senderId ?: "",
@ -110,11 +125,27 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
?: VerificationState.REQUEST ?: VerificationState.REQUEST
ReferencesInfoData(verificationState) ReferencesInfoData(verificationState)
}, },
sentByMe = event.root.senderId == session.myUserId, sentByMe = isSentByMe,
e2eDecoration = e2eDecoration e2eDecoration = e2eDecoration,
sendStateDecoration = sendStateDecoration
) )
} }
private fun getSendStateDecoration(eventSendState: SendState,
prevEventSendState: SendState?,
anyReadReceipts: Boolean,
isMedia: Boolean): SendStateDecoration {
return if (eventSendState.isSending()) {
if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA
} else if (eventSendState.hasFailed()) {
SendStateDecoration.FAILED
} else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) {
SendStateDecoration.SENT
} else {
SendStateDecoration.NONE
}
}
private fun getE2EDecoration(event: TimelineEvent): E2EDecoration { private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
val roomSummary = roomSummariesHolder.get(event.roomId) val roomSummary = roomSummariesHolder.get(event.roomId)
return if ( return if (

View File

@ -19,19 +19,21 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.graphics.Typeface import android.graphics.Typeface
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.ui.views.SendStateImageView
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
/** /**
* Base timeline item that adds an optional information bar with the sender avatar, name and time * Base timeline item that adds an optional information bar with the sender avatar, name, time, send state
* Adds associated click listeners (on avatar, displayname) * Adds associated click listeners (on avatar, displayname)
*/ */
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() { abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() {
@ -82,6 +84,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
} }
// Render send state indicator
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
} }
override fun unbind(holder: H) { override fun unbind(holder: H) {
@ -99,6 +105,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
} }
/** /**

View File

@ -28,7 +28,6 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import org.matrix.android.sdk.api.session.room.send.SendState
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -87,13 +86,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener) holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener)
holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener) holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener)
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
holder.eventSendingIndicator.isVisible = when (attributes.informationData.sendState) {
SendState.UNSENT,
SendState.ENCRYPTING,
SendState.SENDING -> true
else -> false
}
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
@ -111,7 +103,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView) val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView)
val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar) val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar)
val filenameView by bind<TextView>(R.id.messageFilenameView) val filenameView by bind<TextView>(R.id.messageFilenameView)
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
} }
companion object { companion object {

View File

@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -29,7 +28,6 @@ import im.vector.app.core.files.LocalFilesHelper
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.session.room.send.SendState
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() { abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
@ -69,16 +67,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}") ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
holder.mediaContentView.setOnClickListener(attributes.itemClickListener) holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
// The sending state color will be apply to the progress text
renderSendState(holder.imageView, null, holder.failedToSendIndicator)
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
holder.eventSendingIndicator.isVisible = when (attributes.informationData.sendState) {
SendState.UNSENT,
SendState.ENCRYPTING,
SendState.SENDING -> true
else -> false
}
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
@ -96,10 +85,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout) val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
val imageView by bind<ImageView>(R.id.messageThumbnailView) val imageView by bind<ImageView>(R.id.messageThumbnailView)
val playContentView by bind<ImageView>(R.id.messageMediaPlayView) val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia) val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
} }
companion object { companion object {

View File

@ -42,7 +42,8 @@ data class MessageInformationData(
val readReceipts: List<ReadReceiptData> = emptyList(), val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null, val referencesInfoData: ReferencesInfoData? = null,
val sentByMe: Boolean, val sentByMe: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
) : Parcelable { ) : Parcelable {
val matrixItem: MatrixItem val matrixItem: MatrixItem
@ -84,4 +85,12 @@ enum class E2EDecoration {
WARN_SENT_BY_UNKNOWN WARN_SENT_BY_UNKNOWN
} }
enum class SendStateDecoration {
NONE,
SENDING_NON_MEDIA,
SENDING_MEDIA,
SENT,
FAILED
}
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:pathData="M8.5714,22.5C8.5714,23.6864 9.5357,25 10.7143,25H19.2857C20.4643,25 21.4286,23.4428 21.4286,22.2564V11.4711C21.4286,10.2848 20.4643,9.3141 19.2857,9.3141H10.7143C9.5357,9.3141 8.5714,10.2848 8.5714,11.4711V22.5ZM21.4286,6.0785H18.75L17.9893,5.3128C17.7964,5.1186 17.5179,5 17.2393,5H12.7607C12.4821,5 12.2036,5.1186 12.0107,5.3128L11.25,6.0785H8.5714C7.9821,6.0785 7.5,6.5639 7.5,7.1571C7.5,7.7502 7.9821,8.2356 8.5714,8.2356H21.4286C22.0179,8.2356 22.5,7.7502 22.5,7.1571C22.5,6.5639 22.0179,6.0785 21.4286,6.0785Z"
android:fillColor="#FE2928"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,16C12.866,16 16,12.866 16,9C16,5.134 12.866,2 9,2C5.134,2 2,5.134 2,9C2,12.866 5.134,16 9,16ZM9,17C13.4183,17 17,13.4183 17,9C17,4.5817 13.4183,1 9,1C4.5817,1 1,4.5817 1,9C1,13.4183 4.5817,17 9,17Z"
android:fillColor="#8D99A5"
android:fillType="evenOdd"/>
<path
android:pathData="M12.8697,5.9531C12.6784,5.7576 12.3597,5.7473 12.1578,5.9325L7.6207,10.048L5.9524,8.9163C5.7293,8.7722 5.4212,8.7722 5.2087,8.9574C4.9536,9.1632 4.9324,9.5336 5.1449,9.7805L7.0681,11.9206C7.1,11.9515 7.1319,11.9926 7.1744,12.0132C7.5356,12.3013 8.0776,12.2498 8.3751,11.9L8.4069,11.8589L12.891,6.6013C13.0397,6.4161 13.0397,6.1383 12.8697,5.9531Z"
android:fillColor="#8D99A5"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4.0227,2.9646C5.1159,2.1 6.4987,1.5835 8,1.5835C11.3187,1.5835 14.049,4.1029 14.3825,7.3335H15.6723C15.9336,7.3335 16.0894,7.625 15.9445,7.8426L13.9388,10.8543C13.8094,11.0488 13.524,11.0488 13.3945,10.8543L11.3888,7.8426C11.2439,7.625 11.3997,7.3335 11.661,7.3335H12.8719C12.5465,4.9334 10.4893,3.0835 8,3.0835C6.8483,3.0835 5.7909,3.4786 4.9531,4.1411C4.8969,4.1856 4.8485,4.2213 4.813,4.2467C4.7951,4.2595 4.7803,4.2698 4.7692,4.2774L4.7553,4.2869L4.7505,4.2901L4.7487,4.2913L4.7479,4.2918L4.7476,4.2921L4.7474,4.2922L4.7473,4.2922L4.3334,3.6669L4.7472,4.2923C4.4018,4.5209 3.9365,4.4262 3.7079,4.0807C3.4798,3.736 3.5736,3.2719 3.9173,3.0428L3.9202,3.0408L3.9401,3.0268C3.9591,3.0132 3.988,2.992 4.0227,2.9646ZM3.1281,8.6668H4.339C4.6003,8.6668 4.7561,8.3753 4.6112,8.1577L2.6055,5.146C2.476,4.9516 2.1906,4.9516 2.0612,5.146L0.0555,8.1577C-0.0894,8.3753 0.0664,8.6668 0.3277,8.6668H1.6176C1.951,11.8974 4.6813,14.4168 8,14.4168C9.5683,14.4168 11.0069,13.8532 12.1215,12.9184C12.4388,12.6522 12.4803,12.1791 12.2141,11.8617C11.9479,11.5444 11.4749,11.5029 11.1575,11.7691C10.303,12.4859 9.2028,12.9168 8,12.9168C5.5107,12.9168 3.4535,11.0669 3.1281,8.6668Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,16C12.866,16 16,12.866 16,9C16,5.134 12.866,2 9,2C5.134,2 2,5.134 2,9C2,12.866 5.134,16 9,16ZM9,17C13.4183,17 17,13.4183 17,9C17,4.5817 13.4183,1 9,1C4.5817,1 1,4.5817 1,9C1,13.4183 4.5817,17 9,17Z"
android:fillColor="#8D99A5"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16ZM6.9806,4.5101C6.9306,3.9401 7.3506,3.4401 7.9206,3.4001C8.4806,3.3601 8.9806,3.7801 9.0406,4.3501V4.5101L8.7206,8.5101C8.6906,8.8801 8.3806,9.1601 8.0106,9.1601H7.9506C7.6006,9.1301 7.3306,8.8601 7.3006,8.5101L6.9806,4.5101ZM8.8801,11.1202C8.8801,11.6062 8.4861,12.0002 8.0001,12.0002C7.5141,12.0002 7.1201,11.6062 7.1201,11.1202C7.1201,10.6342 7.5141,10.2402 8.0001,10.2402C8.4861,10.2402 8.8801,10.6342 8.8801,11.1202Z"
android:fillColor="#FF4B55"
android:fillType="evenOdd"/>
</vector>

View File

@ -86,7 +86,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="top" app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView" /> app:constraint_referenced_ids="composerLayout,notificationAreaView,failedMessagesWarningView" />
<im.vector.app.features.sync.widget.SyncStateView <im.vector.app.features.sync.widget.SyncStateView
android:id="@+id/syncStateView" android:id="@+id/syncStateView"
@ -159,6 +159,16 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<im.vector.app.core.ui.views.FailedMessagesWarningView
android:id="@+id/failedMessagesWarningView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/composerLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<im.vector.app.features.home.room.detail.composer.TextComposerView <im.vector.app.features.home.room.detail.composer.TextComposerView
android:id="@+id/composerLayout" android:id="@+id/composerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -186,7 +196,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:barrierDirection="top" app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView" /> app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningView" />
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/activeCallPiPWrap" android:id="@+id/activeCallPiPWrap"

View File

@ -8,8 +8,8 @@
<ImageView <ImageView
android:id="@+id/bottom_sheet_message_preview_avatar" android:id="@+id/bottom_sheet_message_preview_avatar"
android:layout_width="60dp" android:layout_width="40dp"
android:layout_height="60dp" android:layout_height="40dp"
android:layout_margin="@dimen/layout_horizontal_margin" android:layout_margin="@dimen/layout_horizontal_margin"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@drawable/circle" android:background="@drawable/circle"
@ -23,7 +23,7 @@
<TextView <TextView
android:id="@+id/bottom_sheet_message_preview_sender" android:id="@+id/bottom_sheet_message_preview_sender"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start" android:layout_gravity="start"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
@ -31,14 +31,24 @@
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="sans-serif-bold" android:fontFamily="sans-serif-bold"
android:singleLine="true" android:singleLine="true"
android:textColor="?riotx_text_primary" android:textColor="@color/riotx_accent"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/bottom_sheet_message_preview_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:text="Friday 8pm" />
<TextView <TextView
android:id="@+id/bottom_sheet_message_preview_body" android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp" android:layout_width="0dp"
@ -52,22 +62,8 @@
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_secondary"
android:textIsSelectable="false" android:textIsSelectable="false"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender" app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " /> tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
<TextView
android:id="@+id/bottom_sheet_message_preview_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
tools:text="Friday 8pm" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -80,6 +80,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/messageMemberNameView" android:layout_below="@id/messageMemberNameView"
android:layout_toStartOf="@id/messageSendStateImageView"
android:layout_toEndOf="@id/messageStartGuideline" android:layout_toEndOf="@id/messageStartGuideline"
android:addStatesFromChildren="true"> android:addStatesFromChildren="true">
@ -133,6 +134,33 @@
</FrameLayout> </FrameLayout>
<im.vector.app.core.ui.views.SendStateImageView
android:id="@+id/messageSendStateImageView"
android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="@dimen/item_event_message_state_size"
android:layout_alignBottom="@+id/viewStubContainer"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:contentDescription="@string/event_status_a11y_sending"
android:src="@drawable/ic_sending_message"
android:visibility="gone"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/eventSendingIndicator"
android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="@dimen/item_event_message_state_size"
android:layout_alignBottom="@+id/viewStubContainer"
android:indeterminateTint="?riotx_text_secondary"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:id="@+id/informationBottom" android:id="@+id/informationBottom"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -55,16 +55,6 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="A filename here" /> tools:text="A filename here" />
<ProgressBar
android:id="@+id/eventSendingIndicator"
style="?android:attr/progressBarStyleSmall"
android:layout_width="16dp"
android:layout_height="16dp"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/messageFilenameView"
app:layout_constraintTop_toTopOf="@id/messageFilenameView"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/horizontalBarrier" android:id="@+id/horizontalBarrier"

View File

@ -18,27 +18,6 @@
tools:layout_height="300dp" tools:layout_height="300dp"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<ImageView
android:id="@+id/messageFailToSendIndicator"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_marginStart="2dp"
android:contentDescription="@string/a11y_error_message_not_sent"
android:src="@drawable/ic_warning_badge"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/messageThumbnailView"
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/eventSendingIndicator"
style="?android:attr/progressBarStyleSmall"
android:layout_width="16dp"
android:layout_height="16dp"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/messageThumbnailView"
app:layout_constraintTop_toBottomOf="@id/messageFailToSendIndicator" />
<ImageView <ImageView
android:id="@+id/messageMediaPlayView" android:id="@+id/messageMediaPlayView"
android:layout_width="40dp" android:layout_width="40dp"

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<View
android:id="@+id/failedMessagesWarningDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/failedMessagesWarningTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="8dp"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="@string/event_status_failed_messages_warning"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:drawableStartCompat="@drawable/ic_sending_message_failed"
app:layout_constraintBottom_toBottomOf="@id/failedMessagesRetryButton"
app:layout_constraintEnd_toStartOf="@+id/failedMessagesDeleteAllButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/failedMessagesRetryButton" />
<ImageButton
android:id="@+id/failedMessagesDeleteAllButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/event_status_a11y_delete_all"
android:src="@drawable/ic_delete_unsent_messages"
app:layout_constraintBottom_toBottomOf="@id/failedMessagesRetryButton"
app:layout_constraintEnd_toStartOf="@id/failedMessagesRetryButton"
app:layout_constraintTop_toTopOf="@id/failedMessagesRetryButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/failedMessagesRetryButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:text="@string/global_retry"
android:textSize="14sp"
app:icon="@drawable/ic_retry_sending_messages"
app:iconTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/failedMessagesWarningDivider" />
</merge>

View File

@ -9,7 +9,7 @@
<TextView <TextView
android:id="@+id/receiptMore" android:id="@+id/receiptMore"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="18dp" android:layout_height="@dimen/item_event_message_state_size"
android:background="@drawable/pill_receipt" android:background="@drawable/pill_receipt"
android:gravity="center" android:gravity="center"
android:importantForAccessibility="no" android:importantForAccessibility="no"
@ -20,8 +20,8 @@
<ImageView <ImageView
android:id="@+id/receiptAvatar5" android:id="@+id/receiptAvatar5"
android:layout_width="18dp" android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="18dp" android:layout_height="@dimen/item_event_message_state_size"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"
@ -30,8 +30,8 @@
<ImageView <ImageView
android:id="@+id/receiptAvatar4" android:id="@+id/receiptAvatar4"
android:layout_width="18dp" android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="18dp" android:layout_height="@dimen/item_event_message_state_size"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"
@ -40,8 +40,8 @@
<ImageView <ImageView
android:id="@+id/receiptAvatar3" android:id="@+id/receiptAvatar3"
android:layout_width="18dp" android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="18dp" android:layout_height="@dimen/item_event_message_state_size"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"
@ -50,8 +50,8 @@
<ImageView <ImageView
android:id="@+id/receiptAvatar2" android:id="@+id/receiptAvatar2"
android:layout_width="18dp" android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="18dp" android:layout_height="@dimen/item_event_message_state_size"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"
@ -60,8 +60,8 @@
<ImageView <ImageView
android:id="@+id/receiptAvatar1" android:id="@+id/receiptAvatar1"
android:layout_width="18dp" android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="18dp" android:layout_height="@dimen/item_event_message_state_size"
android:layout_marginStart="2dp" android:layout_marginStart="2dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:importantForAccessibility="no" android:importantForAccessibility="no"

View File

@ -52,22 +52,6 @@
app:actionLayout="@layout/custom_action_item_layout_badge" app:actionLayout="@layout/custom_action_item_layout_badge"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/resend_all"
android:icon="@drawable/ic_refresh_cw"
android:title="@string/room_prompt_resend"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
<item
android:id="@+id/clear_all"
android:icon="@drawable/ic_trash"
android:title="@string/room_prompt_cancel"
android:visible="false"
app:showAsAction="never"
tools:visible="true" />
<item <item
android:id="@+id/dev_tools" android:id="@+id/dev_tools"
android:icon="@drawable/ic_settings_root_general" android:icon="@drawable/ic_settings_root_general"

View File

@ -13,6 +13,7 @@
<dimen name="navigation_view_height">196dp</dimen> <dimen name="navigation_view_height">196dp</dimen>
<dimen name="navigation_avatar_top_margin">44dp</dimen> <dimen name="navigation_avatar_top_margin">44dp</dimen>
<dimen name="item_decoration_left_margin">72dp</dimen> <dimen name="item_decoration_left_margin">72dp</dimen>
<dimen name="item_event_message_state_size">16dp</dimen>
<dimen name="chat_avatar_size">40dp</dimen> <dimen name="chat_avatar_size">40dp</dimen>
<dimen name="member_list_avatar_size">60dp</dimen> <dimen name="member_list_avatar_size">60dp</dimen>

View File

@ -2051,7 +2051,7 @@
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="reply">Reply</string> <string name="reply">Reply</string>
<string name="global_retry">"Retry"</string> <string name="global_retry">Retry</string>
<string name="room_list_empty">"Join a room to start using the app."</string> <string name="room_list_empty">"Join a room to start using the app."</string>
<string name="send_you_invite">"Sent you an invitation"</string> <string name="send_you_invite">"Sent you an invitation"</string>
<string name="invited_by">Invited by %s</string> <string name="invited_by">Invited by %s</string>
@ -3239,4 +3239,13 @@
<string name="dev_tools_success_event">Event sent!</string> <string name="dev_tools_success_event">Event sent!</string>
<string name="dev_tools_success_state_event">State event sent!</string> <string name="dev_tools_success_state_event">State event sent!</string>
<string name="dev_tools_event_content_hint">Event content</string> <string name="dev_tools_event_content_hint">Event content</string>
<string name="event_status_a11y_sending">Sending</string>
<string name="event_status_a11y_sent">Sent</string>
<string name="event_status_a11y_failed">Failed</string>
<string name="event_status_a11y_delete_all">Delete all failed messages</string>
<string name="event_status_cancel_sending_dialog_message">Do you want to cancel sending message?</string>
<string name="event_status_failed_messages_warning">Messages failed to send</string>
<string name="event_status_delete_all_failed_dialog_title">Delete unsent messages</string>
<string name="event_status_delete_all_failed_dialog_message">Are you sure you want to delete all unsent messages in this room?</string>
</resources> </resources>