Merge pull request #2937 from vector-im/feature/ons/message_states
Improve the status of send messages
This commit is contained in:
commit
8a1a90d1b9
@ -66,11 +66,11 @@ interface RelationService {
|
||||
|
||||
/**
|
||||
* 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 compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||
*/
|
||||
fun editTextMessage(targetEventId: String,
|
||||
fun editTextMessage(targetEvent: TimelineEvent,
|
||||
msgType: String,
|
||||
newBodyText: CharSequence,
|
||||
newBodyAutoMarkdown: Boolean,
|
||||
|
@ -132,4 +132,9 @@ interface SendService {
|
||||
* Resend all failed messages one by one (and keep order)
|
||||
*/
|
||||
fun resendAllFailedMessages()
|
||||
|
||||
/**
|
||||
* Cancel all failed messages
|
||||
*/
|
||||
fun cancelAllFailedMessages()
|
||||
}
|
||||
|
@ -36,9 +36,23 @@ interface TimelineService {
|
||||
*/
|
||||
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
|
||||
|
||||
/**
|
||||
* Returns a snapshot of TimelineEvent event with eventId.
|
||||
* At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
|
||||
* @param eventId the eventId to get the TimelineEvent
|
||||
*/
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
||||
/**
|
||||
* Creates a LiveData of Optional TimelineEvent event with eventId.
|
||||
* If the eventId is a local echo eventId, it will make the LiveData be updated with the synced TimelineEvent when coming through the sync.
|
||||
* In this case, makes sure to use the new synced eventId from the TimelineEvent class if you want to interact, as the local echo is removed from the SDK.
|
||||
* @param eventId the eventId to listen for TimelineEvent
|
||||
*/
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
|
||||
/**
|
||||
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
||||
*/
|
||||
fun getAttachmentMessages(): List<TimelineEvent>
|
||||
}
|
||||
|
@ -51,7 +51,6 @@ internal class DefaultSendEventTask @Inject constructor(
|
||||
|
||||
val event = handleEncryption(params)
|
||||
val localId = event.eventId!!
|
||||
|
||||
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(globalErrorReceiver) {
|
||||
apiCall = roomAPI.send(
|
||||
|
@ -17,14 +17,13 @@ package org.matrix.android.sdk.internal.session.room.relation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
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.session.events.model.Event
|
||||
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.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
@ -47,6 +46,7 @@ import timber.log.Timber
|
||||
|
||||
internal class DefaultRelationService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val eventEditor: EventEditor,
|
||||
private val eventSenderProcessor: EventSenderProcessor,
|
||||
private val eventFactory: LocalEchoEventFactory,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||
@ -112,32 +112,19 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun editTextMessage(targetEventId: String,
|
||||
override fun editTextMessage(targetEvent: TimelineEvent,
|
||||
msgType: String,
|
||||
newBodyText: CharSequence,
|
||||
newBodyAutoMarkdown: Boolean,
|
||||
compatibilityBodyText: String): Cancelable {
|
||||
val event = eventFactory
|
||||
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
||||
.also { saveLocalEcho(it) }
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newBodyAutoMarkdown, compatibilityBodyText)
|
||||
}
|
||||
|
||||
override fun editReply(replyToEdit: TimelineEvent,
|
||||
originalTimelineEvent: TimelineEvent,
|
||||
newBodyText: String,
|
||||
compatibilityBodyText: String): Cancelable {
|
||||
val event = eventFactory.createReplaceTextOfReply(
|
||||
roomId,
|
||||
replyToEdit,
|
||||
originalTimelineEvent,
|
||||
newBodyText,
|
||||
true,
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
compatibilityBodyText
|
||||
)
|
||||
.also { saveLocalEcho(it) }
|
||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||
return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, compatibilityBodyText)
|
||||
}
|
||||
|
||||
override suspend fun fetchEditHistory(eventId: String): List<Event> {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable {
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.matrix.android.sdk.internal.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
@ -31,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||
@ -89,13 +87,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
}
|
||||
|
||||
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
return Transformations.map(liveData) { events ->
|
||||
events.firstOrNull().toOptional()
|
||||
}
|
||||
return LiveTimelineEvent(timelineInput, monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
|
||||
}
|
||||
|
||||
override fun getAttachmentMessages(): List<TimelineEvent> {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -35,11 +35,16 @@ internal class TimelineInput @Inject constructor() {
|
||||
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
|
||||
}
|
||||
|
||||
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncEventId: String) {
|
||||
listeners.toSet().forEach { it.onLocalEchoSynced(roomId, localEchoEventId, syncEventId) }
|
||||
}
|
||||
|
||||
val listeners = mutableSetOf<Listener>()
|
||||
|
||||
internal interface Listener {
|
||||
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent)
|
||||
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState)
|
||||
fun onNewTimelineEvents(roomId: String, eventIds: List<String>)
|
||||
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
|
||||
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit
|
||||
fun onNewTimelineEvents(roomId: String, eventIds: List<String>) = Unit
|
||||
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) = Unit
|
||||
}
|
||||
}
|
||||
|
@ -400,6 +400,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
event.mxDecryptionResult = adapter.fromJson(json)
|
||||
}
|
||||
}
|
||||
timelineInput.onLocalEchoSynced(roomId, it, event.eventId)
|
||||
// Finally delete the local echo
|
||||
sendingEventEntity.deleteOnCascade(true)
|
||||
} else {
|
||||
|
@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
|
||||
# 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
|
||||
enum class===91
|
||||
enum class===92
|
||||
|
||||
### Do not import temporary legacy classes
|
||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -106,4 +106,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
|
||||
|
||||
data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction()
|
||||
|
||||
// Failed messages
|
||||
object RemoveAllFailedMessages : RoomDetailAction()
|
||||
}
|
||||
|
@ -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.KnownCallsViewHolder
|
||||
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.NotificationAreaView
|
||||
import im.vector.app.core.utils.Debouncer
|
||||
@ -325,6 +326,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
setupJumpToBottomView()
|
||||
setupConfBannerView()
|
||||
setupEmojiPopup()
|
||||
setupFailedMessagesWarningView()
|
||||
|
||||
views.roomToolbarContentView.debouncedClicks {
|
||||
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) {
|
||||
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)
|
||||
true
|
||||
}
|
||||
R.id.resend_all -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
|
||||
true
|
||||
}
|
||||
R.id.open_matrix_apps -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
|
||||
true
|
||||
@ -1171,6 +1188,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
val summary = state.asyncRoomSummary()
|
||||
renderToolbar(summary, state.typingMessage)
|
||||
views.activeConferenceView.render(state)
|
||||
views.failedMessagesWarningView.render(state.hasFailedSending)
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
views.jumpToBottomView.count = summary.notificationCount
|
||||
@ -1547,9 +1565,21 @@ class RoomDetailFragment @Inject constructor(
|
||||
MessageActionsBottomSheet
|
||||
.newInstance(roomId, informationData)
|
||||
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
||||
|
||||
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) {
|
||||
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
|
||||
openRoomMemberProfile(informationData.senderId)
|
||||
@ -1745,7 +1775,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
|
||||
}
|
||||
is EventSharedAction.Cancel -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId))
|
||||
handleCancelSend(action)
|
||||
}
|
||||
is EventSharedAction.ReportContentSpam -> {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
|
||||
|
@ -262,66 +262,68 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
when (action) {
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is RoomDetailAction.ComposerFocusChange -> handleComposerFocusChange(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.SendSticker -> handleSendSticker(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
|
||||
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
|
||||
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
|
||||
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
|
||||
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
|
||||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
|
||||
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
|
||||
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
|
||||
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
|
||||
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
|
||||
is RoomDetailAction.CancelSend -> handleCancel(action)
|
||||
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
|
||||
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
|
||||
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
|
||||
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
|
||||
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
|
||||
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
|
||||
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
|
||||
_viewEvents.post(
|
||||
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
|
||||
)
|
||||
}
|
||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
||||
RoomDetailAction.ResendAll -> handleResendAll()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
@ -660,10 +662,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
return@withState false
|
||||
}
|
||||
when (itemId) {
|
||||
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.timeline_setting -> true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
||||
R.id.invite -> state.canInvite
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||
@ -816,7 +816,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId
|
||||
if (inReplyTo != null) {
|
||||
@ -828,7 +828,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val existingBody = messageContent?.body ?: ""
|
||||
if (existingBody != action.text) {
|
||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
|
||||
room.editTextMessage(state.sendMode.timelineEvent,
|
||||
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
||||
action.text,
|
||||
action.autoMarkdown)
|
||||
@ -839,7 +839,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.QUOTE -> {
|
||||
is SendMode.QUOTE -> {
|
||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||
val textMsg = messageContent?.body
|
||||
|
||||
@ -860,7 +860,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
popDraft()
|
||||
}
|
||||
is SendMode.REPLY -> {
|
||||
is SendMode.REPLY -> {
|
||||
state.sendMode.timelineEvent.let {
|
||||
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
|
||||
_viewEvents.post(RoomDetailViewEvents.MessageSent)
|
||||
@ -1223,6 +1223,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
room.resendAllFailedMessages()
|
||||
}
|
||||
|
||||
private fun handleRemoveAllFailedMessages() {
|
||||
room.cancelAllFailedMessages()
|
||||
}
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
// and keep the most recent one to set the read receipt on.
|
||||
@ -1437,7 +1441,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
roomSummariesHolder.set(summary)
|
||||
setState {
|
||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||
copy(typingMessage = typingMessage)
|
||||
copy(
|
||||
typingMessage = typingMessage,
|
||||
hasFailedSending = summary.hasFailedSending
|
||||
)
|
||||
}
|
||||
if (summary.membership == Membership.INVITE) {
|
||||
summary.inviterId?.let { inviterId ->
|
||||
|
@ -75,7 +75,8 @@ data class RoomDetailViewState(
|
||||
val canInvite: Boolean = true,
|
||||
val isAllowedToManageWidgets: Boolean = false,
|
||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||
val showDialerOption: Boolean = false
|
||||
val showDialerOption: Boolean = false,
|
||||
val hasFailedSending: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(
|
||||
|
@ -50,17 +50,8 @@ class MessageColorProvider @Inject constructor(
|
||||
SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color)
|
||||
}
|
||||
} else {
|
||||
// When not in developer mode, we do not use special color for the encrypting state
|
||||
when (sendState) {
|
||||
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)
|
||||
}
|
||||
// When not in developer mode, we use only one color
|
||||
colorProvider.getColorFromAttribute(R.attr.vctr_message_text_color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.epoxy.LoadingItem_
|
||||
import im.vector.app.core.extensions.localDateTime
|
||||
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.home.room.detail.RoomDetailAction
|
||||
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.TimelineEventVisibilityStateChangedListener
|
||||
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.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.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.media.ImageContentRenderer
|
||||
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 {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextOrNull(currentPosition)
|
||||
val prevEvent = items.prevOrNull(currentPosition)
|
||||
if (hasReachedInvite && hasUTD) {
|
||||
return CacheItemData(event.localId, event.root.eventId, null, null, null)
|
||||
}
|
||||
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.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
@ -362,7 +366,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
requestModelBuild()
|
||||
}
|
||||
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? {
|
||||
@ -425,11 +431,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val eventId: String?,
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null
|
||||
val formattedDayModel: DaySeparatorItem? = null,
|
||||
val forceTriggerBuild: Boolean = false
|
||||
) {
|
||||
fun shouldTriggerBuild(): Boolean {
|
||||
// Since those items can change when we paginate, force a re-build
|
||||
return mergedHeaderModel != null || formattedDayModel != null
|
||||
return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.core.extensions.canReact
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
/**
|
||||
@ -56,4 +57,6 @@ data class MessageActionState(
|
||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
||||
|
||||
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
|
||||
|
||||
fun sendState(): SendState? = timelineEvent()?.root?.sendState
|
||||
}
|
||||
|
@ -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.tools.createLinkMovementMethod
|
||||
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
|
||||
|
||||
/**
|
||||
@ -63,23 +65,24 @@ class MessageActionsEpoxyController @Inject constructor(
|
||||
}
|
||||
|
||||
// Send state
|
||||
if (state.informationData.sendState.isSending()) {
|
||||
bottomSheetSendStateItem {
|
||||
id("send_state")
|
||||
showProgress(true)
|
||||
text(stringProvider.getString(R.string.event_status_sending_message))
|
||||
}
|
||||
} else if (state.informationData.sendState.hasFailed()) {
|
||||
val sendState = state.sendState()
|
||||
if (sendState?.hasFailed().orFalse()) {
|
||||
bottomSheetSendStateItem {
|
||||
id("send_state")
|
||||
showProgress(false)
|
||||
text(stringProvider.getString(R.string.unable_to_send_message))
|
||||
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) {
|
||||
E2EDecoration.WARN_IN_CLEAR -> {
|
||||
E2EDecoration.WARN_IN_CLEAR -> {
|
||||
bottomSheetSendStateItem {
|
||||
id("e2e_clear")
|
||||
showProgress(false)
|
||||
|
@ -18,10 +18,11 @@ package im.vector.app.features.home.room.detail.timeline.action
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||
import dagger.Lazy
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.canReact
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
@ -69,13 +70,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
private val vectorPreferences: VectorPreferences
|
||||
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
private val eventId = initialState.eventId
|
||||
private val informationData = initialState.informationData
|
||||
private val room = session.getRoom(initialState.roomId)
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(initialState.roomId)
|
||||
}
|
||||
|
||||
private val eventIdObservable = BehaviorRelay.createDefault(initialState.eventId)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(initialState: MessageActionState): MessageActionsViewModel
|
||||
@ -130,7 +132,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
private fun observeEvent() {
|
||||
if (room == null) return
|
||||
room.rx()
|
||||
.liveTimelineEvent(eventId)
|
||||
.liveTimelineEvent(initialState.eventId)
|
||||
.unwrap()
|
||||
.execute {
|
||||
copy(timelineEvent = it)
|
||||
@ -139,12 +141,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
|
||||
private fun observeReactions() {
|
||||
if (room == null) return
|
||||
room.rx()
|
||||
.liveAnnotationSummary(eventId)
|
||||
.map { annotations ->
|
||||
EmojiDataSource.quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
}
|
||||
eventIdObservable
|
||||
.switchMap { eventId ->
|
||||
room.rx()
|
||||
.liveAnnotationSummary(eventId)
|
||||
.map { annotations ->
|
||||
EmojiDataSource.quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.execute {
|
||||
copy(quickStates = it)
|
||||
@ -154,8 +159,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
private fun observeTimelineEventState() {
|
||||
selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions ->
|
||||
val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe
|
||||
eventIdObservable.accept(nonNullTimelineEvent.eventId)
|
||||
setState {
|
||||
copy(
|
||||
eventId = nonNullTimelineEvent.eventId,
|
||||
messageBody = computeMessageBody(nonNullTimelineEvent),
|
||||
actions = actionsForEvent(nonNullTimelineEvent, permissions)
|
||||
)
|
||||
@ -233,94 +240,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
val msgType = messageContent?.msgType
|
||||
|
||||
return arrayListOf<EventSharedAction>().apply {
|
||||
if (timelineEvent.root.sendState.hasFailed()) {
|
||||
if (canRetry(timelineEvent, actionPermissions)) {
|
||||
add(EventSharedAction.Resend(eventId))
|
||||
when {
|
||||
timelineEvent.root.sendState.hasFailed() -> {
|
||||
addActionsForFailedState(timelineEvent, actionPermissions, messageContent, msgType)
|
||||
}
|
||||
add(EventSharedAction.Remove(eventId))
|
||||
if (vectorPreferences.developerMode()) {
|
||||
addViewSourceItems(timelineEvent)
|
||||
timelineEvent.root.sendState.isSending() -> {
|
||||
addActionsForSendingState(timelineEvent)
|
||||
}
|
||||
} else if (timelineEvent.root.sendState.isSending()) {
|
||||
// TODO is uploading attachment?
|
||||
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))
|
||||
timelineEvent.root.sendState == SendState.SYNCED -> {
|
||||
addActionsForSyncedState(timelineEvent, actionPermissions, messageContent, msgType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
return true
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ class CallItemFactory @Inject constructor(
|
||||
): VectorEpoxyModel<*>? {
|
||||
if (event.root.eventId == null) return null
|
||||
val roomId = event.roomId
|
||||
val informationData = messageInformationDataFactory.create(event, null)
|
||||
val informationData = messageInformationDataFactory.create(event, null, null)
|
||||
val callSignalingContent = event.getCallSignallingContent() ?: return null
|
||||
val callId = callSignalingContent.callId ?: return null
|
||||
val call = callManager.getCallById(callId)
|
||||
|
@ -61,7 +61,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
prevEvent: TimelineEvent?,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
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)
|
||||
return MessageTextItem_()
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
|
@ -48,7 +48,7 @@ class EncryptionItemFactory @Inject constructor(
|
||||
return null
|
||||
}
|
||||
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 isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
|
||||
|
@ -119,13 +119,14 @@ class MessageItemFactory @Inject constructor(
|
||||
}
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
prevEvent: TimelineEvent?,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
roomId = event.roomId
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
|
||||
if (event.root.isRedacted()) {
|
||||
// message is redacted
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||
|
@ -35,7 +35,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val informationData = informationDataFactory.create(event, null)
|
||||
val informationData = informationDataFactory.create(event, null, null)
|
||||
val attributes = NoticeItem.Attributes(
|
||||
avatarRenderer = avatarRenderer,
|
||||
informationData = informationData,
|
||||
|
@ -37,7 +37,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
private val callItemFactory: CallItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
|
||||
/**
|
||||
* Reminder: nextEvent is older and prevEvent is newer.
|
||||
*/
|
||||
fun create(event: TimelineEvent,
|
||||
prevEvent: TimelineEvent?,
|
||||
nextEvent: TimelineEvent?,
|
||||
eventIdToHighlight: String?,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||
@ -46,7 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
val computedModel = try {
|
||||
when (event.root.getClearType()) {
|
||||
EventType.STICKER,
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_NAME,
|
||||
@ -76,9 +80,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
EventType.ENCRYPTED -> {
|
||||
if (event.root.isRedacted()) {
|
||||
// Redacted event, let the MessageItemFactory handle it
|
||||
messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||
} else {
|
||||
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||
}
|
||||
}
|
||||
EventType.STATE_ROOM_ALIASES,
|
||||
|
@ -75,9 +75,9 @@ class VerificationItemFactory @Inject constructor(
|
||||
// If it's not a request ignore this event
|
||||
// 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)
|
||||
|
||||
when (event.root.getClearType()) {
|
||||
|
@ -64,7 +64,7 @@ class WidgetItemFactory @Inject constructor(
|
||||
callback: TimelineEventController.Callback?,
|
||||
widgetContent: WidgetContent,
|
||||
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
|
||||
val informationData = informationDataFactory.create(timelineEvent, null)
|
||||
val informationData = informationDataFactory.create(timelineEvent, null, null)
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||
|
||||
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
|
||||
|
@ -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.ReadReceiptData
|
||||
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 org.matrix.android.sdk.api.crypto.VerificationState
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
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.isAttachmentMessage
|
||||
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.message.MessageVerificationRequestContent
|
||||
@ -49,7 +51,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
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
|
||||
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 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(
|
||||
eventId = eventId,
|
||||
senderId = event.root.senderId ?: "",
|
||||
@ -110,11 +125,27 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
?: VerificationState.REQUEST
|
||||
ReferencesInfoData(verificationState)
|
||||
},
|
||||
sentByMe = event.root.senderId == session.myUserId,
|
||||
e2eDecoration = e2eDecoration
|
||||
sentByMe = isSentByMe,
|
||||
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 {
|
||||
val roomSummary = roomSummariesHolder.get(event.roomId)
|
||||
return if (
|
||||
|
@ -19,19 +19,21 @@ package im.vector.app.features.home.room.detail.timeline.item
|
||||
import android.graphics.Typeface
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.ui.views.SendStateImageView
|
||||
import im.vector.app.core.utils.DebouncedClickListener
|
||||
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.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)
|
||||
*/
|
||||
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.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) {
|
||||
@ -99,6 +105,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
|
||||
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,6 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||
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.ContentUploadStateTrackerBinder
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
@ -87,13 +86,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener)
|
||||
holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
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) {
|
||||
@ -111,7 +103,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView)
|
||||
val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar)
|
||||
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.item
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
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.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
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)
|
||||
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
|
||||
@ -69,16 +67,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
|
||||
holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
|
||||
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.eventSendingIndicator.isVisible = when (attributes.informationData.sendState) {
|
||||
SendState.UNSENT,
|
||||
SendState.ENCRYPTING,
|
||||
SendState.SENDING -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
@ -96,10 +85,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
||||
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||
|
||||
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 {
|
||||
|
@ -42,7 +42,8 @@ data class MessageInformationData(
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val referencesInfoData: ReferencesInfoData? = null,
|
||||
val sentByMe: Boolean,
|
||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE
|
||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
||||
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
|
||||
) : Parcelable {
|
||||
|
||||
val matrixItem: MatrixItem
|
||||
@ -84,4 +85,12 @@ enum class E2EDecoration {
|
||||
WARN_SENT_BY_UNKNOWN
|
||||
}
|
||||
|
||||
enum class SendStateDecoration {
|
||||
NONE,
|
||||
SENDING_NON_MEDIA,
|
||||
SENDING_MEDIA,
|
||||
SENT,
|
||||
FAILED
|
||||
}
|
||||
|
||||
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||
|
@ -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>
|
13
vector/src/main/res/drawable/ic_message_sent.xml
Normal file
13
vector/src/main/res/drawable/ic_message_sent.xml
Normal 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>
|
10
vector/src/main/res/drawable/ic_retry_sending_messages.xml
Normal file
10
vector/src/main/res/drawable/ic_retry_sending_messages.xml
Normal 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>
|
10
vector/src/main/res/drawable/ic_sending_message.xml
Normal file
10
vector/src/main/res/drawable/ic_sending_message.xml
Normal 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>
|
10
vector/src/main/res/drawable/ic_sending_message_failed.xml
Normal file
10
vector/src/main/res/drawable/ic_sending_message_failed.xml
Normal 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>
|
@ -86,7 +86,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView,failedMessagesWarningView" />
|
||||
|
||||
<im.vector.app.features.sync.widget.SyncStateView
|
||||
android:id="@+id/syncStateView"
|
||||
@ -159,6 +159,16 @@
|
||||
app:layout_constraintEnd_toEndOf="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
|
||||
android:id="@+id/composerLayout"
|
||||
android:layout_width="match_parent"
|
||||
@ -186,7 +196,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningView" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/activeCallPiPWrap"
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_message_preview_avatar"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/circle"
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_message_preview_sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:layout_marginStart="8dp"
|
||||
@ -31,14 +31,24 @@
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:singleLine="true"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textColor="@color/riotx_accent"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||
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
|
||||
android:id="@+id/bottom_sheet_message_preview_body"
|
||||
android:layout_width="0dp"
|
||||
@ -52,22 +62,8 @@
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
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. " />
|
||||
|
||||
<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>
|
||||
|
@ -80,6 +80,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/messageMemberNameView"
|
||||
android:layout_toStartOf="@id/messageSendStateImageView"
|
||||
android:layout_toEndOf="@id/messageStartGuideline"
|
||||
android:addStatesFromChildren="true">
|
||||
|
||||
@ -133,6 +134,33 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/informationBottom"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -55,16 +55,6 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
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
|
||||
android:id="@+id/horizontalBarrier"
|
||||
|
@ -18,27 +18,6 @@
|
||||
tools:layout_height="300dp"
|
||||
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
|
||||
android:id="@+id/messageMediaPlayView"
|
||||
android:layout_width="40dp"
|
||||
|
64
vector/src/main/res/layout/view_failed_messages_warning.xml
Normal file
64
vector/src/main/res/layout/view_failed_messages_warning.xml
Normal 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>
|
@ -9,7 +9,7 @@
|
||||
<TextView
|
||||
android:id="@+id/receiptMore"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:layout_height="@dimen/item_event_message_state_size"
|
||||
android:background="@drawable/pill_receipt"
|
||||
android:gravity="center"
|
||||
android:importantForAccessibility="no"
|
||||
@ -20,8 +20,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar5"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_width="@dimen/item_event_message_state_size"
|
||||
android:layout_height="@dimen/item_event_message_state_size"
|
||||
android:layout_marginStart="2dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:importantForAccessibility="no"
|
||||
@ -30,8 +30,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar4"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_width="@dimen/item_event_message_state_size"
|
||||
android:layout_height="@dimen/item_event_message_state_size"
|
||||
android:layout_marginStart="2dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:importantForAccessibility="no"
|
||||
@ -40,8 +40,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar3"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_width="@dimen/item_event_message_state_size"
|
||||
android:layout_height="@dimen/item_event_message_state_size"
|
||||
android:layout_marginStart="2dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:importantForAccessibility="no"
|
||||
@ -50,8 +50,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar2"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_width="@dimen/item_event_message_state_size"
|
||||
android:layout_height="@dimen/item_event_message_state_size"
|
||||
android:layout_marginStart="2dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:importantForAccessibility="no"
|
||||
@ -60,8 +60,8 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar1"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_width="@dimen/item_event_message_state_size"
|
||||
android:layout_height="@dimen/item_event_message_state_size"
|
||||
android:layout_marginStart="2dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:importantForAccessibility="no"
|
||||
|
@ -52,22 +52,6 @@
|
||||
app:actionLayout="@layout/custom_action_item_layout_badge"
|
||||
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
|
||||
android:id="@+id/dev_tools"
|
||||
android:icon="@drawable/ic_settings_root_general"
|
||||
|
@ -13,6 +13,7 @@
|
||||
<dimen name="navigation_view_height">196dp</dimen>
|
||||
<dimen name="navigation_avatar_top_margin">44dp</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="member_list_avatar_size">60dp</dimen>
|
||||
|
@ -2051,7 +2051,7 @@
|
||||
<string name="edit">Edit</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="send_you_invite">"Sent you an invitation"</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_state_event">State event sent!</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>
|
||||
|
Loading…
Reference in New Issue
Block a user