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
|
* Edit a text message body. Limited to "m.text" contentType
|
||||||
* @param targetEventId The event to edit
|
* @param targetEvent The event to edit
|
||||||
* @param newBodyText The edited body
|
* @param newBodyText The edited body
|
||||||
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||||
*/
|
*/
|
||||||
fun editTextMessage(targetEventId: String,
|
fun editTextMessage(targetEvent: TimelineEvent,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: CharSequence,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
|
@ -132,4 +132,9 @@ interface SendService {
|
|||||||
* Resend all failed messages one by one (and keep order)
|
* Resend all failed messages one by one (and keep order)
|
||||||
*/
|
*/
|
||||||
fun resendAllFailedMessages()
|
fun resendAllFailedMessages()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel all failed messages
|
||||||
|
*/
|
||||||
|
fun cancelAllFailedMessages()
|
||||||
}
|
}
|
||||||
|
@ -36,9 +36,23 @@ interface TimelineService {
|
|||||||
*/
|
*/
|
||||||
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
|
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot of TimelineEvent event with eventId.
|
||||||
|
* At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
|
||||||
|
* @param eventId the eventId to get the TimelineEvent
|
||||||
|
*/
|
||||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a LiveData of Optional TimelineEvent event with eventId.
|
||||||
|
* If the eventId is a local echo eventId, it will make the LiveData be updated with the synced TimelineEvent when coming through the sync.
|
||||||
|
* In this case, makes sure to use the new synced eventId from the TimelineEvent class if you want to interact, as the local echo is removed from the SDK.
|
||||||
|
* @param eventId the eventId to listen for TimelineEvent
|
||||||
|
*/
|
||||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO.
|
||||||
|
*/
|
||||||
fun getAttachmentMessages(): List<TimelineEvent>
|
fun getAttachmentMessages(): List<TimelineEvent>
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,6 @@ internal class DefaultSendEventTask @Inject constructor(
|
|||||||
|
|
||||||
val event = handleEncryption(params)
|
val event = handleEncryption(params)
|
||||||
val localId = event.eventId!!
|
val localId = event.eventId!!
|
||||||
|
|
||||||
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
|
localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
|
||||||
val executeRequest = executeRequest<SendResponse>(globalErrorReceiver) {
|
val executeRequest = executeRequest<SendResponse>(globalErrorReceiver) {
|
||||||
apiCall = roomAPI.send(
|
apiCall = roomAPI.send(
|
||||||
|
@ -17,14 +17,13 @@ package org.matrix.android.sdk.internal.session.room.relation
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
|
import org.matrix.android.sdk.api.session.room.model.relation.RelationService
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
@ -47,6 +46,7 @@ import timber.log.Timber
|
|||||||
|
|
||||||
internal class DefaultRelationService @AssistedInject constructor(
|
internal class DefaultRelationService @AssistedInject constructor(
|
||||||
@Assisted private val roomId: String,
|
@Assisted private val roomId: String,
|
||||||
|
private val eventEditor: EventEditor,
|
||||||
private val eventSenderProcessor: EventSenderProcessor,
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
@ -112,32 +112,19 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editTextMessage(targetEventId: String,
|
override fun editTextMessage(targetEvent: TimelineEvent,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: CharSequence,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String): Cancelable {
|
compatibilityBodyText: String): Cancelable {
|
||||||
val event = eventFactory
|
return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newBodyAutoMarkdown, compatibilityBodyText)
|
||||||
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
|
|
||||||
.also { saveLocalEcho(it) }
|
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editReply(replyToEdit: TimelineEvent,
|
override fun editReply(replyToEdit: TimelineEvent,
|
||||||
originalTimelineEvent: TimelineEvent,
|
originalTimelineEvent: TimelineEvent,
|
||||||
newBodyText: String,
|
newBodyText: String,
|
||||||
compatibilityBodyText: String): Cancelable {
|
compatibilityBodyText: String): Cancelable {
|
||||||
val event = eventFactory.createReplaceTextOfReply(
|
return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, compatibilityBodyText)
|
||||||
roomId,
|
|
||||||
replyToEdit,
|
|
||||||
originalTimelineEvent,
|
|
||||||
newBodyText,
|
|
||||||
true,
|
|
||||||
MessageType.MSGTYPE_TEXT,
|
|
||||||
compatibilityBodyText
|
|
||||||
)
|
|
||||||
.also { saveLocalEcho(it) }
|
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchEditHistory(eventId: String): List<Event> {
|
override suspend fun fetchEditHistory(eventId: String): List<Event> {
|
||||||
|
@ -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,
|
override fun sendMedia(attachment: ContentAttachmentData,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable {
|
roomIds: Set<String>): Cancelable {
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package org.matrix.android.sdk.internal.session.room.timeline
|
package org.matrix.android.sdk.internal.session.room.timeline
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
@ -31,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.api.util.toOptional
|
|
||||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||||
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
|
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||||
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
|
||||||
@ -89,13 +87,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
||||||
val liveData = monarchy.findAllMappedWithChanges(
|
return LiveTimelineEvent(timelineInput, monarchy, taskExecutor.executorScope, timelineEventMapper, roomId, eventId)
|
||||||
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
|
|
||||||
{ timelineEventMapper.map(it) }
|
|
||||||
)
|
|
||||||
return Transformations.map(liveData) { events ->
|
|
||||||
events.firstOrNull().toOptional()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttachmentMessages(): List<TimelineEvent> {
|
override fun getAttachmentMessages(): List<TimelineEvent> {
|
||||||
|
@ -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) }
|
listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncEventId: String) {
|
||||||
|
listeners.toSet().forEach { it.onLocalEchoSynced(roomId, localEchoEventId, syncEventId) }
|
||||||
|
}
|
||||||
|
|
||||||
val listeners = mutableSetOf<Listener>()
|
val listeners = mutableSetOf<Listener>()
|
||||||
|
|
||||||
internal interface Listener {
|
internal interface Listener {
|
||||||
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent)
|
fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
|
||||||
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState)
|
fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit
|
||||||
fun onNewTimelineEvents(roomId: String, eventIds: List<String>)
|
fun onNewTimelineEvents(roomId: String, eventIds: List<String>) = Unit
|
||||||
|
fun onLocalEchoSynced(roomId: String, localEchoEventId: String, syncedEventId: String) = Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -400,6 +400,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
event.mxDecryptionResult = adapter.fromJson(json)
|
event.mxDecryptionResult = adapter.fromJson(json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
timelineInput.onLocalEchoSynced(roomId, it, event.eventId)
|
||||||
// Finally delete the local echo
|
// Finally delete the local echo
|
||||||
sendingEventEntity.deleteOnCascade(true)
|
sendingEventEntity.deleteOnCascade(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
|
|||||||
# android\.text\.TextUtils
|
# android\.text\.TextUtils
|
||||||
|
|
||||||
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
|
||||||
enum class===91
|
enum class===92
|
||||||
|
|
||||||
### Do not import temporary legacy classes
|
### Do not import temporary legacy classes
|
||||||
import org.matrix.android.sdk.internal.legacy.riot===3
|
import org.matrix.android.sdk.internal.legacy.riot===3
|
||||||
|
@ -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 DoNotShowPreviewUrlFor(val eventId: String, val url: String) : RoomDetailAction()
|
||||||
|
|
||||||
data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction()
|
data class ComposerFocusChange(val focused: Boolean) : RoomDetailAction()
|
||||||
|
|
||||||
|
// Failed messages
|
||||||
|
object RemoveAllFailedMessages : RoomDetailAction()
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,7 @@ import im.vector.app.core.resources.ColorProvider
|
|||||||
import im.vector.app.core.ui.views.CurrentCallsView
|
import im.vector.app.core.ui.views.CurrentCallsView
|
||||||
import im.vector.app.core.ui.views.KnownCallsViewHolder
|
import im.vector.app.core.ui.views.KnownCallsViewHolder
|
||||||
import im.vector.app.core.ui.views.ActiveConferenceView
|
import im.vector.app.core.ui.views.ActiveConferenceView
|
||||||
|
import im.vector.app.core.ui.views.FailedMessagesWarningView
|
||||||
import im.vector.app.core.ui.views.JumpToReadMarkerView
|
import im.vector.app.core.ui.views.JumpToReadMarkerView
|
||||||
import im.vector.app.core.ui.views.NotificationAreaView
|
import im.vector.app.core.ui.views.NotificationAreaView
|
||||||
import im.vector.app.core.utils.Debouncer
|
import im.vector.app.core.utils.Debouncer
|
||||||
@ -325,6 +326,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
setupJumpToBottomView()
|
setupJumpToBottomView()
|
||||||
setupConfBannerView()
|
setupConfBannerView()
|
||||||
setupEmojiPopup()
|
setupEmojiPopup()
|
||||||
|
setupFailedMessagesWarningView()
|
||||||
|
|
||||||
views.roomToolbarContentView.debouncedClicks {
|
views.roomToolbarContentView.debouncedClicks {
|
||||||
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
||||||
@ -557,6 +559,25 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupFailedMessagesWarningView() {
|
||||||
|
views.failedMessagesWarningView.callback = object : FailedMessagesWarningView.Callback {
|
||||||
|
override fun onDeleteAllClicked() {
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.event_status_delete_all_failed_dialog_title)
|
||||||
|
.setMessage(getString(R.string.event_status_delete_all_failed_dialog_message))
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.RemoveAllFailedMessages)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRetryClicked() {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
|
private fun joinJitsiRoom(jitsiWidget: Widget, enableVideo: Boolean) {
|
||||||
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
|
navigator.openRoomWidget(requireContext(), roomDetailArgs.roomId, jitsiWidget, mapOf(JitsiCallViewModel.ENABLE_VIDEO_OPTION to enableVideo))
|
||||||
}
|
}
|
||||||
@ -776,10 +797,6 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.resend_all -> {
|
|
||||||
roomDetailViewModel.handle(RoomDetailAction.ResendAll)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.open_matrix_apps -> {
|
R.id.open_matrix_apps -> {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
|
roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
|
||||||
true
|
true
|
||||||
@ -1171,6 +1188,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
val summary = state.asyncRoomSummary()
|
val summary = state.asyncRoomSummary()
|
||||||
renderToolbar(summary, state.typingMessage)
|
renderToolbar(summary, state.typingMessage)
|
||||||
views.activeConferenceView.render(state)
|
views.activeConferenceView.render(state)
|
||||||
|
views.failedMessagesWarningView.render(state.hasFailedSending)
|
||||||
val inviter = state.asyncInviter()
|
val inviter = state.asyncInviter()
|
||||||
if (summary?.membership == Membership.JOIN) {
|
if (summary?.membership == Membership.JOIN) {
|
||||||
views.jumpToBottomView.count = summary.notificationCount
|
views.jumpToBottomView.count = summary.notificationCount
|
||||||
@ -1547,9 +1565,21 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
MessageActionsBottomSheet
|
MessageActionsBottomSheet
|
||||||
.newInstance(roomId, informationData)
|
.newInstance(roomId, informationData)
|
||||||
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCancelSend(action: EventSharedAction.Cancel) {
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(R.string.dialog_title_confirmation)
|
||||||
|
.setMessage(getString(R.string.event_status_cancel_sending_dialog_message))
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.setPositiveButton(R.string.yes) { _, _ ->
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId))
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||||
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
|
// roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId))
|
||||||
openRoomMemberProfile(informationData.senderId)
|
openRoomMemberProfile(informationData.senderId)
|
||||||
@ -1745,7 +1775,7 @@ class RoomDetailFragment @Inject constructor(
|
|||||||
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
|
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
|
||||||
}
|
}
|
||||||
is EventSharedAction.Cancel -> {
|
is EventSharedAction.Cancel -> {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.CancelSend(action.eventId))
|
handleCancelSend(action)
|
||||||
}
|
}
|
||||||
is EventSharedAction.ReportContentSpam -> {
|
is EventSharedAction.ReportContentSpam -> {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
|
roomDetailViewModel.handle(RoomDetailAction.ReportContent(
|
||||||
|
@ -322,6 +322,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action)
|
||||||
|
RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages()
|
||||||
|
RoomDetailAction.ResendAll -> handleResendAll()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,10 +662,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
return@withState false
|
return@withState false
|
||||||
}
|
}
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
|
||||||
R.id.timeline_setting -> true
|
R.id.timeline_setting -> true
|
||||||
R.id.invite -> state.canInvite
|
R.id.invite -> state.canInvite
|
||||||
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
|
|
||||||
R.id.open_matrix_apps -> true
|
R.id.open_matrix_apps -> true
|
||||||
R.id.voice_call,
|
R.id.voice_call,
|
||||||
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
|
||||||
@ -828,7 +828,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
val messageContent = state.sendMode.timelineEvent.getLastMessageContent()
|
||||||
val existingBody = messageContent?.body ?: ""
|
val existingBody = messageContent?.body ?: ""
|
||||||
if (existingBody != action.text) {
|
if (existingBody != action.text) {
|
||||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
|
room.editTextMessage(state.sendMode.timelineEvent,
|
||||||
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
messageContent?.msgType ?: MessageType.MSGTYPE_TEXT,
|
||||||
action.text,
|
action.text,
|
||||||
action.autoMarkdown)
|
action.autoMarkdown)
|
||||||
@ -1223,6 +1223,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
room.resendAllFailedMessages()
|
room.resendAllFailedMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleRemoveAllFailedMessages() {
|
||||||
|
room.cancelAllFailedMessages()
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeEventDisplayedActions() {
|
private fun observeEventDisplayedActions() {
|
||||||
// We are buffering scroll events for one second
|
// We are buffering scroll events for one second
|
||||||
// and keep the most recent one to set the read receipt on.
|
// and keep the most recent one to set the read receipt on.
|
||||||
@ -1437,7 +1441,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||||||
roomSummariesHolder.set(summary)
|
roomSummariesHolder.set(summary)
|
||||||
setState {
|
setState {
|
||||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||||
copy(typingMessage = typingMessage)
|
copy(
|
||||||
|
typingMessage = typingMessage,
|
||||||
|
hasFailedSending = summary.hasFailedSending
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (summary.membership == Membership.INVITE) {
|
if (summary.membership == Membership.INVITE) {
|
||||||
summary.inviterId?.let { inviterId ->
|
summary.inviterId?.let { inviterId ->
|
||||||
|
@ -75,7 +75,8 @@ data class RoomDetailViewState(
|
|||||||
val canInvite: Boolean = true,
|
val canInvite: Boolean = true,
|
||||||
val isAllowedToManageWidgets: Boolean = false,
|
val isAllowedToManageWidgets: Boolean = false,
|
||||||
val isAllowedToStartWebRTCCall: Boolean = true,
|
val isAllowedToStartWebRTCCall: Boolean = true,
|
||||||
val showDialerOption: Boolean = false
|
val showDialerOption: Boolean = false,
|
||||||
|
val hasFailedSending: Boolean = false
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(
|
constructor(args: RoomDetailArgs) : this(
|
||||||
|
@ -50,17 +50,8 @@ class MessageColorProvider @Inject constructor(
|
|||||||
SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color)
|
SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When not in developer mode, we do not use special color for the encrypting state
|
// When not in developer mode, we use only one color
|
||||||
when (sendState) {
|
colorProvider.getColorFromAttribute(R.attr.vctr_message_text_color)
|
||||||
SendState.UNKNOWN,
|
|
||||||
SendState.UNSENT,
|
|
||||||
SendState.ENCRYPTING,
|
|
||||||
SendState.SENDING -> colorProvider.getColorFromAttribute(R.attr.vctr_sending_message_text_color)
|
|
||||||
SendState.SENT,
|
|
||||||
SendState.SYNCED -> colorProvider.getColorFromAttribute(R.attr.vctr_message_text_color)
|
|
||||||
SendState.UNDELIVERED,
|
|
||||||
SendState.FAILED_UNKNOWN_DEVICES -> colorProvider.getColorFromAttribute(R.attr.vctr_unsent_message_text_color)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import im.vector.app.core.date.VectorDateFormatter
|
|||||||
import im.vector.app.core.epoxy.LoadingItem_
|
import im.vector.app.core.epoxy.LoadingItem_
|
||||||
import im.vector.app.core.extensions.localDateTime
|
import im.vector.app.core.extensions.localDateTime
|
||||||
import im.vector.app.core.extensions.nextOrNull
|
import im.vector.app.core.extensions.nextOrNull
|
||||||
|
import im.vector.app.core.extensions.prevOrNull
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailViewState
|
import im.vector.app.features.home.room.detail.RoomDetailViewState
|
||||||
@ -42,11 +43,13 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineControlle
|
|||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
import im.vector.app.features.media.VideoContentRenderer
|
||||||
@ -336,11 +339,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||||
val event = items[currentPosition]
|
val event = items[currentPosition]
|
||||||
val nextEvent = items.nextOrNull(currentPosition)
|
val nextEvent = items.nextOrNull(currentPosition)
|
||||||
|
val prevEvent = items.prevOrNull(currentPosition)
|
||||||
if (hasReachedInvite && hasUTD) {
|
if (hasReachedInvite && hasUTD) {
|
||||||
return CacheItemData(event.localId, event.root.eventId, null, null, null)
|
return CacheItemData(event.localId, event.root.eventId, null, null, null)
|
||||||
}
|
}
|
||||||
updateUTDStates(event, nextEvent)
|
updateUTDStates(event, nextEvent)
|
||||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also {
|
||||||
it.id(event.localId)
|
it.id(event.localId)
|
||||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||||
}
|
}
|
||||||
@ -362,7 +366,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
|
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
|
||||||
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
|
// If we have a SENT decoration, we want to built again as it might have to be changed to NONE if more recent event has also SENT decoration
|
||||||
|
val forceTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
|
||||||
|
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, forceTriggerBuild)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
|
private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
|
||||||
@ -425,11 +431,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
val eventModel: EpoxyModel<*>? = null,
|
val eventModel: EpoxyModel<*>? = null,
|
||||||
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
||||||
val formattedDayModel: DaySeparatorItem? = null
|
val formattedDayModel: DaySeparatorItem? = null,
|
||||||
|
val forceTriggerBuild: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun shouldTriggerBuild(): Boolean {
|
fun shouldTriggerBuild(): Boolean {
|
||||||
// Since those items can change when we paginate, force a re-build
|
// Since those items can change when we paginate, force a re-build
|
||||||
return mergedHeaderModel != null || formattedDayModel != null
|
return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import com.airbnb.mvrx.MvRxState
|
|||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.app.core.extensions.canReact
|
import im.vector.app.core.extensions.canReact
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,4 +57,6 @@ data class MessageActionState(
|
|||||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
||||||
|
|
||||||
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
|
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
|
||||||
|
|
||||||
|
fun sendState(): SendState? = timelineEvent()?.root?.sendState
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,8 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|||||||
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,19 +65,20 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send state
|
// Send state
|
||||||
if (state.informationData.sendState.isSending()) {
|
val sendState = state.sendState()
|
||||||
bottomSheetSendStateItem {
|
if (sendState?.hasFailed().orFalse()) {
|
||||||
id("send_state")
|
|
||||||
showProgress(true)
|
|
||||||
text(stringProvider.getString(R.string.event_status_sending_message))
|
|
||||||
}
|
|
||||||
} else if (state.informationData.sendState.hasFailed()) {
|
|
||||||
bottomSheetSendStateItem {
|
bottomSheetSendStateItem {
|
||||||
id("send_state")
|
id("send_state")
|
||||||
showProgress(false)
|
showProgress(false)
|
||||||
text(stringProvider.getString(R.string.unable_to_send_message))
|
text(stringProvider.getString(R.string.unable_to_send_message))
|
||||||
drawableStart(R.drawable.ic_warning_badge)
|
drawableStart(R.drawable.ic_warning_badge)
|
||||||
}
|
}
|
||||||
|
} else if (sendState != SendState.SYNCED) {
|
||||||
|
bottomSheetSendStateItem {
|
||||||
|
id("send_state")
|
||||||
|
showProgress(true)
|
||||||
|
text(stringProvider.getString(R.string.event_status_sending_message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (state.informationData.e2eDecoration) {
|
when (state.informationData.e2eDecoration) {
|
||||||
|
@ -18,10 +18,11 @@ package im.vector.app.features.home.room.detail.timeline.action
|
|||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.canReact
|
import im.vector.app.core.extensions.canReact
|
||||||
import im.vector.app.core.platform.EmptyViewEvents
|
import im.vector.app.core.platform.EmptyViewEvents
|
||||||
@ -69,13 +70,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
private val vectorPreferences: VectorPreferences
|
private val vectorPreferences: VectorPreferences
|
||||||
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
private val eventId = initialState.eventId
|
|
||||||
private val informationData = initialState.informationData
|
private val informationData = initialState.informationData
|
||||||
private val room = session.getRoom(initialState.roomId)
|
private val room = session.getRoom(initialState.roomId)
|
||||||
private val pillsPostProcessor by lazy {
|
private val pillsPostProcessor by lazy {
|
||||||
pillsPostProcessorFactory.create(initialState.roomId)
|
pillsPostProcessorFactory.create(initialState.roomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val eventIdObservable = BehaviorRelay.createDefault(initialState.eventId)
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: MessageActionState): MessageActionsViewModel
|
fun create(initialState: MessageActionState): MessageActionsViewModel
|
||||||
@ -130,7 +132,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
private fun observeEvent() {
|
private fun observeEvent() {
|
||||||
if (room == null) return
|
if (room == null) return
|
||||||
room.rx()
|
room.rx()
|
||||||
.liveTimelineEvent(eventId)
|
.liveTimelineEvent(initialState.eventId)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.execute {
|
.execute {
|
||||||
copy(timelineEvent = it)
|
copy(timelineEvent = it)
|
||||||
@ -139,6 +141,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
|
|
||||||
private fun observeReactions() {
|
private fun observeReactions() {
|
||||||
if (room == null) return
|
if (room == null) return
|
||||||
|
eventIdObservable
|
||||||
|
.switchMap { eventId ->
|
||||||
room.rx()
|
room.rx()
|
||||||
.liveAnnotationSummary(eventId)
|
.liveAnnotationSummary(eventId)
|
||||||
.map { annotations ->
|
.map { annotations ->
|
||||||
@ -146,6 +150,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.execute {
|
.execute {
|
||||||
copy(quickStates = it)
|
copy(quickStates = it)
|
||||||
}
|
}
|
||||||
@ -154,8 +159,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
private fun observeTimelineEventState() {
|
private fun observeTimelineEventState() {
|
||||||
selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions ->
|
selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions ->
|
||||||
val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe
|
val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe
|
||||||
|
eventIdObservable.accept(nonNullTimelineEvent.eventId)
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
|
eventId = nonNullTimelineEvent.eventId,
|
||||||
messageBody = computeMessageBody(nonNullTimelineEvent),
|
messageBody = computeMessageBody(nonNullTimelineEvent),
|
||||||
actions = actionsForEvent(nonNullTimelineEvent, permissions)
|
actions = actionsForEvent(nonNullTimelineEvent, permissions)
|
||||||
)
|
)
|
||||||
@ -233,20 +240,62 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
val msgType = messageContent?.msgType
|
val msgType = messageContent?.msgType
|
||||||
|
|
||||||
return arrayListOf<EventSharedAction>().apply {
|
return arrayListOf<EventSharedAction>().apply {
|
||||||
if (timelineEvent.root.sendState.hasFailed()) {
|
when {
|
||||||
|
timelineEvent.root.sendState.hasFailed() -> {
|
||||||
|
addActionsForFailedState(timelineEvent, actionPermissions, messageContent, msgType)
|
||||||
|
}
|
||||||
|
timelineEvent.root.sendState.isSending() -> {
|
||||||
|
addActionsForSendingState(timelineEvent)
|
||||||
|
}
|
||||||
|
timelineEvent.root.sendState == SendState.SYNCED -> {
|
||||||
|
addActionsForSyncedState(timelineEvent, actionPermissions, messageContent, msgType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ArrayList<EventSharedAction>.addViewSourceItems(timelineEvent: TimelineEvent) {
|
||||||
|
add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
|
||||||
|
if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult != null) {
|
||||||
|
val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
|
||||||
|
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||||
|
add(EventSharedAction.ViewDecryptedSource(decryptedContent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ArrayList<EventSharedAction>.addActionsForFailedState(timelineEvent: TimelineEvent,
|
||||||
|
actionPermissions: ActionPermissions,
|
||||||
|
messageContent: MessageContent?,
|
||||||
|
msgType: String?) {
|
||||||
|
val eventId = timelineEvent.eventId
|
||||||
if (canRetry(timelineEvent, actionPermissions)) {
|
if (canRetry(timelineEvent, actionPermissions)) {
|
||||||
add(EventSharedAction.Resend(eventId))
|
add(EventSharedAction.Resend(eventId))
|
||||||
}
|
}
|
||||||
add(EventSharedAction.Remove(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()) {
|
if (vectorPreferences.developerMode()) {
|
||||||
addViewSourceItems(timelineEvent)
|
addViewSourceItems(timelineEvent)
|
||||||
}
|
}
|
||||||
} else if (timelineEvent.root.sendState.isSending()) {
|
}
|
||||||
|
|
||||||
|
private fun ArrayList<EventSharedAction>.addActionsForSendingState(timelineEvent: TimelineEvent) {
|
||||||
// TODO is uploading attachment?
|
// TODO is uploading attachment?
|
||||||
if (canCancel(timelineEvent)) {
|
if (canCancel(timelineEvent)) {
|
||||||
add(EventSharedAction.Cancel(eventId))
|
add(EventSharedAction.Cancel(timelineEvent.eventId))
|
||||||
}
|
}
|
||||||
} else if (timelineEvent.root.sendState == SendState.SYNCED) {
|
}
|
||||||
|
|
||||||
|
private fun ArrayList<EventSharedAction>.addActionsForSyncedState(timelineEvent: TimelineEvent,
|
||||||
|
actionPermissions: ActionPermissions,
|
||||||
|
messageContent: MessageContent?,
|
||||||
|
msgType: String?) {
|
||||||
|
val eventId = timelineEvent.eventId
|
||||||
if (!timelineEvent.root.isRedacted()) {
|
if (!timelineEvent.root.isRedacted()) {
|
||||||
if (canReply(timelineEvent, messageContent, actionPermissions)) {
|
if (canReply(timelineEvent, messageContent, actionPermissions)) {
|
||||||
add(EventSharedAction.Reply(eventId))
|
add(EventSharedAction.Reply(eventId))
|
||||||
@ -323,17 +372,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||||||
add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
|
add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ArrayList<EventSharedAction>.addViewSourceItems(timelineEvent: TimelineEvent) {
|
|
||||||
add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
|
|
||||||
if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult != null) {
|
|
||||||
val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
|
|
||||||
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
|
||||||
add(EventSharedAction.ViewDecryptedSource(decryptedContent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
|
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
|
||||||
return true
|
return true
|
||||||
|
@ -52,7 +52,7 @@ class CallItemFactory @Inject constructor(
|
|||||||
): VectorEpoxyModel<*>? {
|
): VectorEpoxyModel<*>? {
|
||||||
if (event.root.eventId == null) return null
|
if (event.root.eventId == null) return null
|
||||||
val roomId = event.roomId
|
val roomId = event.roomId
|
||||||
val informationData = messageInformationDataFactory.create(event, null)
|
val informationData = messageInformationDataFactory.create(event, null, null)
|
||||||
val callSignalingContent = event.getCallSignallingContent() ?: return null
|
val callSignalingContent = event.getCallSignallingContent() ?: return null
|
||||||
val callId = callSignalingContent.callId ?: return null
|
val callId = callSignalingContent.callId ?: return null
|
||||||
val call = callManager.getCallById(callId)
|
val call = callManager.getCallById(callId)
|
||||||
|
@ -61,7 +61,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||||||
} else {
|
} else {
|
||||||
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
|
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
|
||||||
}
|
}
|
||||||
val informationData = informationDataFactory.create(event, null)
|
val informationData = informationDataFactory.create(event, null, null)
|
||||||
return create(text, informationData, highlight, callback)
|
return create(text, informationData, highlight, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
private val vectorPreferences: VectorPreferences) {
|
private val vectorPreferences: VectorPreferences) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
|
prevEvent: TimelineEvent?,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||||
@ -108,7 +109,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
|
||||||
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
|
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
@ -48,7 +48,7 @@ class EncryptionItemFactory @Inject constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
|
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
|
||||||
val informationData = informationDataFactory.create(event, null)
|
val informationData = informationDataFactory.create(event, null, null)
|
||||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||||
|
|
||||||
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
|
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
@ -119,13 +119,14 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
|
prevEvent: TimelineEvent?,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?
|
callback: TimelineEventController.Callback?
|
||||||
): VectorEpoxyModel<*>? {
|
): VectorEpoxyModel<*>? {
|
||||||
event.root.eventId ?: return null
|
event.root.eventId ?: return null
|
||||||
roomId = event.roomId
|
roomId = event.roomId
|
||||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
|
||||||
if (event.root.isRedacted()) {
|
if (event.root.isRedacted()) {
|
||||||
// message is redacted
|
// message is redacted
|
||||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||||
|
@ -35,7 +35,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
|||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||||
val formattedText = eventFormatter.format(event) ?: return null
|
val formattedText = eventFormatter.format(event) ?: return null
|
||||||
val informationData = informationDataFactory.create(event, null)
|
val informationData = informationDataFactory.create(event, null, null)
|
||||||
val attributes = NoticeItem.Attributes(
|
val attributes = NoticeItem.Attributes(
|
||||||
avatarRenderer = avatarRenderer,
|
avatarRenderer = avatarRenderer,
|
||||||
informationData = informationData,
|
informationData = informationData,
|
||||||
|
@ -37,7 +37,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
private val callItemFactory: CallItemFactory,
|
private val callItemFactory: CallItemFactory,
|
||||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reminder: nextEvent is older and prevEvent is newer.
|
||||||
|
*/
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
|
prevEvent: TimelineEvent?,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||||
@ -46,7 +50,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
val computedModel = try {
|
val computedModel = try {
|
||||||
when (event.root.getClearType()) {
|
when (event.root.getClearType()) {
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||||
// State and call
|
// State and call
|
||||||
EventType.STATE_ROOM_TOMBSTONE,
|
EventType.STATE_ROOM_TOMBSTONE,
|
||||||
EventType.STATE_ROOM_NAME,
|
EventType.STATE_ROOM_NAME,
|
||||||
@ -76,9 +80,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
EventType.ENCRYPTED -> {
|
EventType.ENCRYPTED -> {
|
||||||
if (event.root.isRedacted()) {
|
if (event.root.isRedacted()) {
|
||||||
// Redacted event, let the MessageItemFactory handle it
|
// Redacted event, let the MessageItemFactory handle it
|
||||||
messageItemFactory.create(event, nextEvent, highlight, callback)
|
messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||||
} else {
|
} else {
|
||||||
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventType.STATE_ROOM_ALIASES,
|
EventType.STATE_ROOM_ALIASES,
|
||||||
|
@ -75,9 +75,9 @@ class VerificationItemFactory @Inject constructor(
|
|||||||
// If it's not a request ignore this event
|
// If it's not a request ignore this event
|
||||||
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
|
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
|
||||||
|
|
||||||
val referenceInformationData = messageInformationDataFactory.create(refEvent, null)
|
val referenceInformationData = messageInformationDataFactory.create(refEvent, null, null)
|
||||||
|
|
||||||
val informationData = messageInformationDataFactory.create(event, null)
|
val informationData = messageInformationDataFactory.create(event, null, null)
|
||||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||||
|
|
||||||
when (event.root.getClearType()) {
|
when (event.root.getClearType()) {
|
||||||
|
@ -64,7 +64,7 @@ class WidgetItemFactory @Inject constructor(
|
|||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
widgetContent: WidgetContent,
|
widgetContent: WidgetContent,
|
||||||
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
|
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
|
||||||
val informationData = informationDataFactory.create(timelineEvent, null)
|
val informationData = informationDataFactory.create(timelineEvent, null, null)
|
||||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||||
|
|
||||||
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
|
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
|
||||||
|
@ -25,11 +25,13 @@ import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
|||||||
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
|
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import org.matrix.android.sdk.api.crypto.VerificationState
|
import org.matrix.android.sdk.api.crypto.VerificationState
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
|
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
@ -49,7 +51,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val vectorPreferences: VectorPreferences) {
|
private val vectorPreferences: VectorPreferences) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData {
|
||||||
// Non nullability has been tested before
|
// Non nullability has been tested before
|
||||||
val eventId = event.root.eventId!!
|
val eventId = event.root.eventId!!
|
||||||
|
|
||||||
@ -70,6 +72,19 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
||||||
val e2eDecoration = getE2EDecoration(event)
|
val e2eDecoration = getE2EDecoration(event)
|
||||||
|
|
||||||
|
// SendState Decoration
|
||||||
|
val isSentByMe = event.root.senderId == session.myUserId
|
||||||
|
val sendStateDecoration = if (isSentByMe) {
|
||||||
|
getSendStateDecoration(
|
||||||
|
eventSendState = event.root.sendState,
|
||||||
|
prevEventSendState = prevEvent?.root?.sendState,
|
||||||
|
anyReadReceipts = event.readReceipts.any { it.user.userId != session.myUserId },
|
||||||
|
isMedia = event.root.isAttachmentMessage()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SendStateDecoration.NONE
|
||||||
|
}
|
||||||
|
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
@ -110,11 +125,27 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
?: VerificationState.REQUEST
|
?: VerificationState.REQUEST
|
||||||
ReferencesInfoData(verificationState)
|
ReferencesInfoData(verificationState)
|
||||||
},
|
},
|
||||||
sentByMe = event.root.senderId == session.myUserId,
|
sentByMe = isSentByMe,
|
||||||
e2eDecoration = e2eDecoration
|
e2eDecoration = e2eDecoration,
|
||||||
|
sendStateDecoration = sendStateDecoration
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSendStateDecoration(eventSendState: SendState,
|
||||||
|
prevEventSendState: SendState?,
|
||||||
|
anyReadReceipts: Boolean,
|
||||||
|
isMedia: Boolean): SendStateDecoration {
|
||||||
|
return if (eventSendState.isSending()) {
|
||||||
|
if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA
|
||||||
|
} else if (eventSendState.hasFailed()) {
|
||||||
|
SendStateDecoration.FAILED
|
||||||
|
} else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) {
|
||||||
|
SendStateDecoration.SENT
|
||||||
|
} else {
|
||||||
|
SendStateDecoration.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
|
private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
|
||||||
val roomSummary = roomSummariesHolder.get(event.roomId)
|
val roomSummary = roomSummariesHolder.get(event.roomId)
|
||||||
return if (
|
return if (
|
||||||
|
@ -19,19 +19,21 @@ package im.vector.app.features.home.room.detail.timeline.item
|
|||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.ui.views.SendStateImageView
|
||||||
import im.vector.app.core.utils.DebouncedClickListener
|
import im.vector.app.core.utils.DebouncedClickListener
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base timeline item that adds an optional information bar with the sender avatar, name and time
|
* Base timeline item that adds an optional information bar with the sender avatar, name, time, send state
|
||||||
* Adds associated click listeners (on avatar, displayname)
|
* Adds associated click listeners (on avatar, displayname)
|
||||||
*/
|
*/
|
||||||
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() {
|
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() {
|
||||||
@ -82,6 +84,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||||||
holder.avatarImageView.setOnLongClickListener(null)
|
holder.avatarImageView.setOnLongClickListener(null)
|
||||||
holder.memberNameView.setOnLongClickListener(null)
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render send state indicator
|
||||||
|
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||||
|
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: H) {
|
override fun unbind(holder: H) {
|
||||||
@ -99,6 +105,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
|
||||||
|
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +28,6 @@ import com.airbnb.epoxy.EpoxyModelClass
|
|||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||||
@ -87,13 +86,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||||||
holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener)
|
holder.fileImageWrapper.setOnClickListener(attributes.itemClickListener)
|
||||||
holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.fileImageWrapper.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
|
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
|
||||||
|
|
||||||
holder.eventSendingIndicator.isVisible = when (attributes.informationData.sendState) {
|
|
||||||
SendState.UNSENT,
|
|
||||||
SendState.ENCRYPTING,
|
|
||||||
SendState.SENDING -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
@ -111,7 +103,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||||||
val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView)
|
val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView)
|
||||||
val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar)
|
val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar)
|
||||||
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||||
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.item
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
@ -29,7 +28,6 @@ import im.vector.app.core.files.LocalFilesHelper
|
|||||||
import im.vector.app.core.glide.GlideApp
|
import im.vector.app.core.glide.GlideApp
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
|
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
|
||||||
@ -69,16 +67,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||||||
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
|
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
|
||||||
holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
|
holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
|
||||||
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
// The sending state color will be apply to the progress text
|
|
||||||
renderSendState(holder.imageView, null, holder.failedToSendIndicator)
|
|
||||||
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
holder.eventSendingIndicator.isVisible = when (attributes.informationData.sendState) {
|
|
||||||
SendState.UNSENT,
|
|
||||||
SendState.ENCRYPTING,
|
|
||||||
SendState.SENDING -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
@ -96,10 +85,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||||
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
||||||
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||||
|
|
||||||
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
|
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
|
||||||
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
|
|
||||||
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -42,7 +42,8 @@ data class MessageInformationData(
|
|||||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||||
val referencesInfoData: ReferencesInfoData? = null,
|
val referencesInfoData: ReferencesInfoData? = null,
|
||||||
val sentByMe: Boolean,
|
val sentByMe: Boolean,
|
||||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE
|
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
||||||
|
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
val matrixItem: MatrixItem
|
val matrixItem: MatrixItem
|
||||||
@ -84,4 +85,12 @@ enum class E2EDecoration {
|
|||||||
WARN_SENT_BY_UNKNOWN
|
WARN_SENT_BY_UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SendStateDecoration {
|
||||||
|
NONE,
|
||||||
|
SENDING_NON_MEDIA,
|
||||||
|
SENDING_MEDIA,
|
||||||
|
SENT,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||||
|
@ -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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="top"
|
app:barrierDirection="top"
|
||||||
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
app:constraint_referenced_ids="composerLayout,notificationAreaView,failedMessagesWarningView" />
|
||||||
|
|
||||||
<im.vector.app.features.sync.widget.SyncStateView
|
<im.vector.app.features.sync.widget.SyncStateView
|
||||||
android:id="@+id/syncStateView"
|
android:id="@+id/syncStateView"
|
||||||
@ -159,6 +159,16 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.FailedMessagesWarningView
|
||||||
|
android:id="@+id/failedMessagesWarningView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composerLayout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<im.vector.app.features.home.room.detail.composer.TextComposerView
|
<im.vector.app.features.home.room.detail.composer.TextComposerView
|
||||||
android:id="@+id/composerLayout"
|
android:id="@+id/composerLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -186,7 +196,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:barrierDirection="top"
|
app:barrierDirection="top"
|
||||||
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningView" />
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/activeCallPiPWrap"
|
android:id="@+id/activeCallPiPWrap"
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/bottom_sheet_message_preview_avatar"
|
android:id="@+id/bottom_sheet_message_preview_avatar"
|
||||||
android:layout_width="60dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="60dp"
|
android:layout_height="40dp"
|
||||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:background="@drawable/circle"
|
android:background="@drawable/circle"
|
||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/bottom_sheet_message_preview_sender"
|
android:id="@+id/bottom_sheet_message_preview_sender"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
@ -31,14 +31,24 @@
|
|||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:fontFamily="sans-serif-bold"
|
android:fontFamily="sans-serif-bold"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textColor="?riotx_text_primary"
|
android:textColor="@color/riotx_accent"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||||
tools:text="@tools:sample/full_names" />
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_message_preview_timestamp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||||
|
tools:text="Friday 8pm" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/bottom_sheet_message_preview_body"
|
android:id="@+id/bottom_sheet_message_preview_body"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -52,22 +62,8 @@
|
|||||||
android:textColor="?riotx_text_secondary"
|
android:textColor="?riotx_text_secondary"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
|
||||||
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
|
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bottom_sheet_message_preview_timestamp"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:textColor="?riotx_text_secondary"
|
|
||||||
android:textSize="12sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
|
|
||||||
tools:text="Friday 8pm" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -80,6 +80,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/messageMemberNameView"
|
android:layout_below="@id/messageMemberNameView"
|
||||||
|
android:layout_toStartOf="@id/messageSendStateImageView"
|
||||||
android:layout_toEndOf="@id/messageStartGuideline"
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
android:addStatesFromChildren="true">
|
android:addStatesFromChildren="true">
|
||||||
|
|
||||||
@ -133,6 +134,33 @@
|
|||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.SendStateImageView
|
||||||
|
android:id="@+id/messageSendStateImageView"
|
||||||
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_alignBottom="@+id/viewStubContainer"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:contentDescription="@string/event_status_a11y_sending"
|
||||||
|
android:src="@drawable/ic_sending_message"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/eventSendingIndicator"
|
||||||
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_alignBottom="@+id/viewStubContainer"
|
||||||
|
android:indeterminateTint="?riotx_text_secondary"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/informationBottom"
|
android:id="@+id/informationBottom"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -55,16 +55,6 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="A filename here" />
|
tools:text="A filename here" />
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/eventSendingIndicator"
|
|
||||||
style="?android:attr/progressBarStyleSmall"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageFilenameView"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/messageFilenameView"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/horizontalBarrier"
|
android:id="@+id/horizontalBarrier"
|
||||||
|
@ -18,27 +18,6 @@
|
|||||||
tools:layout_height="300dp"
|
tools:layout_height="300dp"
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/messageFailToSendIndicator"
|
|
||||||
android:layout_width="14dp"
|
|
||||||
android:layout_height="14dp"
|
|
||||||
android:layout_marginStart="2dp"
|
|
||||||
android:contentDescription="@string/a11y_error_message_not_sent"
|
|
||||||
android:src="@drawable/ic_warning_badge"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageThumbnailView"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/eventSendingIndicator"
|
|
||||||
style="?android:attr/progressBarStyleSmall"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageThumbnailView"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/messageFailToSendIndicator" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageMediaPlayView"
|
android:id="@+id/messageMediaPlayView"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
|
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
|
<TextView
|
||||||
android:id="@+id/receiptMore"
|
android:id="@+id/receiptMore"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="18dp"
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
android:background="@drawable/pill_receipt"
|
android:background="@drawable/pill_receipt"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/receiptAvatar5"
|
android:id="@+id/receiptAvatar5"
|
||||||
android:layout_width="18dp"
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
android:layout_height="18dp"
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
@ -30,8 +30,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/receiptAvatar4"
|
android:id="@+id/receiptAvatar4"
|
||||||
android:layout_width="18dp"
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
android:layout_height="18dp"
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
@ -40,8 +40,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/receiptAvatar3"
|
android:id="@+id/receiptAvatar3"
|
||||||
android:layout_width="18dp"
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
android:layout_height="18dp"
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
@ -50,8 +50,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/receiptAvatar2"
|
android:id="@+id/receiptAvatar2"
|
||||||
android:layout_width="18dp"
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
android:layout_height="18dp"
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
@ -60,8 +60,8 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/receiptAvatar1"
|
android:id="@+id/receiptAvatar1"
|
||||||
android:layout_width="18dp"
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
android:layout_height="18dp"
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
|
@ -52,22 +52,6 @@
|
|||||||
app:actionLayout="@layout/custom_action_item_layout_badge"
|
app:actionLayout="@layout/custom_action_item_layout_badge"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/resend_all"
|
|
||||||
android:icon="@drawable/ic_refresh_cw"
|
|
||||||
android:title="@string/room_prompt_resend"
|
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="never"
|
|
||||||
tools:visible="true" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/clear_all"
|
|
||||||
android:icon="@drawable/ic_trash"
|
|
||||||
android:title="@string/room_prompt_cancel"
|
|
||||||
android:visible="false"
|
|
||||||
app:showAsAction="never"
|
|
||||||
tools:visible="true" />
|
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/dev_tools"
|
android:id="@+id/dev_tools"
|
||||||
android:icon="@drawable/ic_settings_root_general"
|
android:icon="@drawable/ic_settings_root_general"
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<dimen name="navigation_view_height">196dp</dimen>
|
<dimen name="navigation_view_height">196dp</dimen>
|
||||||
<dimen name="navigation_avatar_top_margin">44dp</dimen>
|
<dimen name="navigation_avatar_top_margin">44dp</dimen>
|
||||||
<dimen name="item_decoration_left_margin">72dp</dimen>
|
<dimen name="item_decoration_left_margin">72dp</dimen>
|
||||||
|
<dimen name="item_event_message_state_size">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="chat_avatar_size">40dp</dimen>
|
<dimen name="chat_avatar_size">40dp</dimen>
|
||||||
<dimen name="member_list_avatar_size">60dp</dimen>
|
<dimen name="member_list_avatar_size">60dp</dimen>
|
||||||
|
@ -2051,7 +2051,7 @@
|
|||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="reply">Reply</string>
|
<string name="reply">Reply</string>
|
||||||
|
|
||||||
<string name="global_retry">"Retry"</string>
|
<string name="global_retry">Retry</string>
|
||||||
<string name="room_list_empty">"Join a room to start using the app."</string>
|
<string name="room_list_empty">"Join a room to start using the app."</string>
|
||||||
<string name="send_you_invite">"Sent you an invitation"</string>
|
<string name="send_you_invite">"Sent you an invitation"</string>
|
||||||
<string name="invited_by">Invited by %s</string>
|
<string name="invited_by">Invited by %s</string>
|
||||||
@ -3239,4 +3239,13 @@
|
|||||||
<string name="dev_tools_success_event">Event sent!</string>
|
<string name="dev_tools_success_event">Event sent!</string>
|
||||||
<string name="dev_tools_success_state_event">State event sent!</string>
|
<string name="dev_tools_success_state_event">State event sent!</string>
|
||||||
<string name="dev_tools_event_content_hint">Event content</string>
|
<string name="dev_tools_event_content_hint">Event content</string>
|
||||||
|
|
||||||
|
<string name="event_status_a11y_sending">Sending</string>
|
||||||
|
<string name="event_status_a11y_sent">Sent</string>
|
||||||
|
<string name="event_status_a11y_failed">Failed</string>
|
||||||
|
<string name="event_status_a11y_delete_all">Delete all failed messages</string>
|
||||||
|
<string name="event_status_cancel_sending_dialog_message">Do you want to cancel sending message?</string>
|
||||||
|
<string name="event_status_failed_messages_warning">Messages failed to send</string>
|
||||||
|
<string name="event_status_delete_all_failed_dialog_title">Delete unsent messages</string>
|
||||||
|
<string name="event_status_delete_all_failed_dialog_message">Are you sure you want to delete all unsent messages in this room?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user