diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index e115ab8ac2..82a4a762ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -22,11 +22,21 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class ContentAttachmentData( val size: Long = 0, - val duration: Long = 0, + val duration: Long? = 0, val date: Long = 0, - val height: Long = 0, - val width: Long = 0, + val height: Long? = 0, + val width: Long? = 0, val name: String? = null, val path: String? = null, - val mimeType: String? = null -) : Parcelable \ No newline at end of file + val mimeType: String? = null, + val type: Type +) : Parcelable { + + enum class Type { + FILE, + IMAGE, + AUDIO, + VIDEO + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt index 1a0768df56..701a62728b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/VideoInfo.kt @@ -22,8 +22,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VideoInfo( @Json(name = "mimetype") val mimeType: String, - @Json(name = "w") val w: Int = 0, - @Json(name = "h") val h: Int = 0, + @Json(name = "w") val width: Int = 0, + @Json(name = "h") val height: Int = 0, @Json(name = "size") val size: Long = 0, @Json(name = "duration") val duration: Int = 0, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 6f6a18c8f2..ff7ee11b3b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -16,10 +16,8 @@ package im.vector.matrix.android.api.session.room.send -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to send events in a room. It's implemented at the room level. @@ -29,11 +27,23 @@ interface SendService { /** * Method to send a text message asynchronously. * @param text the text message to send - * @param callback the callback to be notified. * @return a [Cancelable] */ - fun sendTextMessage(text: String, callback: MatrixCallback): Cancelable + fun sendTextMessage(text: String): Cancelable + + /** + * Method to send a media asynchronously. + * @param attachment the media to send + * @return a [Cancelable] + */ + fun sendMedia(attachment: ContentAttachmentData): Cancelable + + /** + * Method to send a list of media asynchronously. + * @param attachments the list of media to send + * @return a [Cancelable] + */ + fun sendMedias(attachments: List): Cancelable - fun sendMedia(attachment: ContentAttachmentData, callback: MatrixCallback): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt new file mode 100644 index 0000000000..b5b2264e85 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.matrix.android.api.session.room.send + +enum class SendState { + UNKNOWN, + UNSENT, + ENCRYPTING, + SENDING, + SENT, + SYNCED; + + fun isSent(): Boolean { + return this == SENT || this == SYNCED + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ffd7dbc65c..ca971b653d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.send.SendState /** * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. @@ -28,7 +29,8 @@ data class TimelineEvent( val root: Event, val localId: String, val displayIndex: Int, - val roomMember: RoomMember? + val roomMember: RoomMember?, + val sendState: SendState ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt new file mode 100644 index 0000000000..8db44148d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmQueryLatch.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 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.matrix.android.internal.database + +import android.os.Handler +import android.os.HandlerThread +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmConfiguration +import io.realm.RealmObject +import io.realm.RealmQuery +import io.realm.RealmResults +import java.util.concurrent.CountDownLatch + +private const val THREAD_NAME = "REALM_QUERY_LATCH" + +class RealmQueryLatch(private val realmConfiguration: RealmConfiguration, + private val realmQueryBuilder: (Realm) -> RealmQuery) { + + fun await() { + val latch = CountDownLatch(1) + val handlerThread = HandlerThread(THREAD_NAME + hashCode()) + handlerThread.start() + val handler = Handler(handlerThread.looper) + val runnable = Runnable { + val realm = Realm.getInstance(realmConfiguration) + val result = realmQueryBuilder(realm).findAllAsync() + result.addChangeListener(object : RealmChangeListener> { + override fun onChange(t: RealmResults) { + if (t.isNotEmpty()) { + result.removeChangeListener(this) + realm.close() + latch.countDown() + } + } + }) + } + handler.post(runnable) + latch.await() + handlerThread.quit() + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 850fd43137..6df95e107f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -89,9 +90,10 @@ internal fun ChunkEntity.add(roomId: String, isUnlinked: Boolean = false) { assertIsManaged() - if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) { + if (event.eventId.isNullOrEmpty() || this.events.fastContains(event.eventId)) { return } + var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { currentDisplayIndex += 1 @@ -115,6 +117,7 @@ internal fun ChunkEntity.add(roomId: String, this.stateIndex = currentStateIndex this.isUnlinked = isUnlinked this.displayIndex = currentDisplayIndex + this.sendState = SendState.SYNCED } // We are not using the order of the list, but will be sorting with displayIndex field events.add(eventEntity) @@ -122,14 +125,14 @@ internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index dedce53970..7cb6124c0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.database.helper import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.internal.database.mapper.toEntity import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity @@ -49,6 +50,16 @@ internal fun RoomEntity.addStateEvents(stateEvents: List, this.stateIndex = stateIndex this.isUnlinked = isUnlinked } - untimelinedStateEvents.add(eventEntity) + untimelinedStateEvents.add(0, eventEntity) } } + +internal fun RoomEntity.addSendingEvent(event: Event, + stateIndex: Int) { + assertIsManaged() + val eventEntity = event.toEntity(roomId).apply { + this.sendState = SendState.UNSENT + this.stateIndex = stateIndex + } + sendingTimelineEvents.add(0, eventEntity) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index c688a11c43..c2fa36c17b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -16,12 +16,15 @@ package im.vector.matrix.android.internal.database.model +import im.vector.matrix.android.api.session.room.send.SendState import io.realm.RealmObject import io.realm.RealmResults +import io.realm.annotations.Ignore import io.realm.annotations.Index import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey import java.util.* +import kotlin.properties.Delegates internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), @Index var eventId: String = "", @@ -45,6 +48,13 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI BOTH } + private var sendStateStr: String = SendState.UNKNOWN.name + + @delegate:Ignore + var sendState: SendState by Delegates.observable(SendState.valueOf(sendStateStr)) { _, _, newValue -> + sendStateStr = newValue.name + } + companion object @LinkingObjects("events") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt index e9293c0ee6..6c61d0de69 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt @@ -26,6 +26,7 @@ import kotlin.properties.Delegates internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), var untimelinedStateEvents: RealmList = RealmList(), + var sendingTimelineEvents: RealmList = RealmList(), var areAllMembersLoaded: Boolean = false ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 77793e49e3..ccbe68409d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -24,22 +24,25 @@ import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.util.Cancelable -import im.vector.matrix.android.internal.database.helper.add +import im.vector.matrix.android.api.util.CancelableBag +import im.vector.matrix.android.api.util.addTo +import im.vector.matrix.android.internal.database.helper.addSendingEvent import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker -import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.tryTransactionAsync import java.util.concurrent.TimeUnit private const val SEND_WORK = "SEND_WORK" +private const val UPLOAD_WORK = "UPLOAD_WORK" private const val BACKOFF_DELAY = 10_000L private val WORK_CONSTRAINTS = Constraints.Builder() @@ -48,21 +51,30 @@ private val WORK_CONSTRAINTS = Constraints.Builder() internal class DefaultSendService(private val roomId: String, private val eventFactory: LocalEchoEventFactory, - private val monarchy: Monarchy) : SendService { + private val monarchy: Monarchy) + : SendService { - - override fun sendTextMessage(text: String, callback: MatrixCallback): Cancelable { - val event = eventFactory.createTextEvent(roomId, text) - saveLocalEcho(event) + override fun sendTextMessage(text: String): Cancelable { + val event = eventFactory.createTextEvent(roomId, text).also { + saveLocalEcho(it) + } val sendWork = createSendEventWork(event) WorkManager.getInstance() - .beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, sendWork) + .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) .enqueue() return CancelableWork(sendWork.id) } - override fun sendMedia(attachment: ContentAttachmentData, callback: MatrixCallback): Cancelable { + override fun sendMedias(attachments: List): Cancelable { + val cancelableBag = CancelableBag() + attachments.forEach { + sendMedia(it).addTo(cancelableBag) + } + return cancelableBag + } + + override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path val event = eventFactory.createMediaEvent(roomId, attachment).also { saveLocalEcho(it) @@ -71,20 +83,28 @@ internal class DefaultSendService(private val roomId: String, val sendWork = createSendEventWork(event) WorkManager.getInstance() - .beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, uploadWork) + .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .then(sendWork) .enqueue() + return CancelableWork(sendWork.id) } private fun saveLocalEcho(event: Event) { monarchy.tryTransactionAsync { realm -> - val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - ?: return@tryTransactionAsync - chunkEntity.add(roomId, event, PaginationDirection.FORWARDS) + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + ?: return@tryTransactionAsync + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) + ?: return@tryTransactionAsync + + roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) } } + private fun buildWorkIdentifier(identifier: String): String { + return "${roomId}_$identifier" + } + private fun createSendEventWork(event: Event): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 4986ff9d19..a7216c6f26 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -21,10 +21,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toContent -import im.vector.matrix.android.api.session.room.model.message.ImageInfo -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.* internal class LocalEchoEventFactory(private val credentials: Credentials) { @@ -34,13 +31,22 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { } fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { + return when (attachment.type) { + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) + ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) + } + } + + private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { val content = MessageImageContent( type = MessageType.MSGTYPE_IMAGE, body = attachment.name ?: "image", info = ImageInfo( mimeType = attachment.mimeType ?: "image/png", - width = attachment.width.toInt(), - height = attachment.height.toInt(), + width = attachment.width?.toInt() ?: 0, + height = attachment.height?.toInt() ?: 0, size = attachment.size.toInt() ), url = attachment.path @@ -48,6 +54,48 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { return createEvent(roomId, content) } + private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageVideoContent( + type = MessageType.MSGTYPE_VIDEO, + body = attachment.name ?: "video", + info = VideoInfo( + mimeType = attachment.mimeType ?: "video/mpeg", + width = attachment.width?.toInt() ?: 0, + height = attachment.height?.toInt() ?: 0, + size = attachment.size, + duration = attachment.duration?.toInt() ?: 0 + ), + url = attachment.path + ) + return createEvent(roomId, content) + } + + private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageAudioContent( + type = MessageType.MSGTYPE_AUDIO, + body = attachment.name ?: "audio", + info = AudioInfo( + mimeType = attachment.mimeType ?: "audio/mpeg", + size = attachment.size + ), + url = attachment.path + ) + return createEvent(roomId, content) + } + + private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event { + val content = MessageFileContent( + type = MessageType.MSGTYPE_FILE, + body = attachment.name ?: "file", + info = FileInfo( + mimeType = attachment.mimeType ?: "application/octet-stream", + size = attachment.size + ), + url = attachment.path + ) + return createEvent(roomId, content) + } + private fun createEvent(roomId: String, content: Any? = null): Event { return Event( roomId = roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index d956186ba8..864ef13e0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -20,15 +20,11 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionSync import org.koin.standalone.inject internal class SendEventWorker(context: Context, params: WorkerParameters) @@ -42,32 +38,25 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) ) private val roomAPI by inject() - private val monarchy by inject() override fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - val event = params.event - if (event.eventId == null) { + val localEvent = params.event + if (localEvent.eventId == null) { return Result.failure() } val result = executeRequest { apiCall = roomAPI.send( - event.eventId, + localEvent.eventId, params.roomId, - event.type, - event.content + localEvent.type, + localEvent.content ) } - result.flatMap { sendResponse -> - monarchy.tryTransactionSync { realm -> - val dummyEventEntity = EventEntity.where(realm, params.event.eventId).findFirst() - dummyEventEntity?.eventId = sendResponse.eventId - } - } return result.fold({ Result.retry() }, { Result.success() }) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 38555d2027..89a8307b7f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,25 +27,19 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import im.vector.matrix.android.internal.util.Debouncer +import io.realm.* import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference +import kotlin.collections.ArrayList private const val INITIAL_LOAD_SIZE = 20 @@ -68,8 +62,7 @@ internal class DefaultTimeline( set(value) { field = value backgroundHandler.get()?.post { - val snapshot = snapshot() - mainHandler.post { listener?.onUpdated(snapshot) } + postSnapshot() } } @@ -80,41 +73,46 @@ internal class DefaultTimeline( private val mainHandler = Handler(Looper.getMainLooper()) private val backgroundRealm = AtomicReference() private val cancelableBag = CancelableBag() + private val debouncer = Debouncer(mainHandler) private lateinit var liveEvents: RealmResults + private var roomEntity: RoomEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) - private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - // TODO HANDLE CHANGES - changeSet.insertionRanges.forEach { range -> - val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) - } else { - Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) - } - val state = getPaginationState(direction) - if (state.isPaginating) { - // We are getting new items from pagination - val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount) - if (shouldPostSnapshot) { + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { + handleInitialLoad() + } else { + changeSet.insertionRanges.forEach { range -> + val (startDisplayIndex, direction) = if (range.startIndex == 0) { + Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) + } else { + Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) + } + val state = getPaginationState(direction) + if (state.isPaginating) { + // We are getting new items from pagination + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount) + if (shouldPostSnapshot) { + postSnapshot() + } + } else { + // We are getting new items from sync + buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) postSnapshot() } - } else { - // We are getting new items from sync - buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) - postSnapshot() } } } - // Public methods ****************************************************************************** +// Public methods ****************************************************************************** override fun paginate(direction: Timeline.Direction, count: Int) { backgroundHandler.get()?.post { @@ -142,12 +140,19 @@ internal class DefaultTimeline( val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) clearUnlinkedEvents(realm) - isReady.set(true) + + roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()?.also { + it.sendingTimelineEvents.addChangeListener { _ -> + postSnapshot() + } + } + liveEvents = buildEventQuery(realm) .sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - .findAll() + .findAllAsync() .also { it.addChangeListener(eventsChangeListener) } - handleInitialLoad() + + isReady.set(true) } } } @@ -171,7 +176,7 @@ internal class DefaultTimeline( return hasMoreInCache(direction) || !hasReachedEnd(direction) } - // Private methods ***************************************************************************** +// Private methods ***************************************************************************** private fun hasMoreInCache(direction: Timeline.Direction): Boolean { val localRealm = Realm.getInstance(realmConfiguration) @@ -203,7 +208,7 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it access realm live results - * @return true if snapshot should be posted + * @return true if createSnapshot should be posted */ private fun paginateInternal(startDisplayIndex: Int, direction: Timeline.Direction, @@ -222,8 +227,19 @@ internal class DefaultTimeline( return !shouldFetchMore } - private fun snapshot(): List { - return builtEvents.toList() + private fun createSnapshot(): List { + return buildSendingEvents() + builtEvents.toList() + } + + private fun buildSendingEvents(): List { + val sendingEvents = ArrayList() + if (hasReachedEnd(Timeline.Direction.FORWARDS)) { + roomEntity?.sendingTimelineEvents?.forEach { + val timelineEvent = timelineEventFactory.create(it) + sendingEvents.add(timelineEvent) + } + } + return sendingEvents } private fun canPaginate(direction: Timeline.Direction): Boolean { @@ -282,9 +298,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) @@ -414,8 +430,9 @@ internal class DefaultTimeline( } private fun postSnapshot() { - val snapshot = snapshot() - mainHandler.post { listener?.onUpdated(snapshot) } + val snapshot = createSnapshot() + val runnable = Runnable { listener?.onUpdated(snapshot) } + debouncer.debounce("post_snapshot", runnable, 50) } // Extension methods *************************************************************************** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index d748fe7fc7..20c0ebee68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -29,7 +29,8 @@ internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberE eventEntity.asDomain(), eventEntity.localId, eventEntity.displayIndex, - roomMember + roomMember, + eventEntity.sendState ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 5692961863..24dff3398a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.lastStateIndex import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater @@ -108,6 +109,15 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, timelineStateOffset ) roomEntity.addOrUpdate(chunkEntity) + + // Try to remove local echo + val transactionIds = roomSync.timeline.events.mapNotNull { it.unsignedData?.transactionId } + transactionIds.forEach { + val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) + if (sendingEventEntity != null) { + roomEntity.sendingTimelineEvents.remove(sendingEventEntity) + } + } } roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications) @@ -161,7 +171,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, } else { realm.createObject().apply { this.prevToken = prevToken } } - lastChunk?.isLastForward = false chunkEntity.isLastForward = true chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt new file mode 100644 index 0000000000..2f9921b98f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Debouncer.kt @@ -0,0 +1,45 @@ +/* + * + * * Copyright 2019 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.matrix.android.internal.util + +import android.os.Handler + +internal class Debouncer(private val handler: Handler) { + + private val runnables = HashMap() + + fun debounce(identifier: String, r: Runnable, millis: Long): Boolean { + if (runnables.containsKey(identifier)) { + // debounce + val old = runnables[identifier] + handler.removeCallbacks(old) + } + insertRunnable(identifier, r, millis) + return true + } + + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { + val chained = Runnable { + handler.post(r) + runnables.remove(identifier) + } + runnables[identifier] = chained + handler.postDelayed(chained, millis) + } +} \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index d7ef8c9cea..fe5d41e1be 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -209,7 +209,7 @@ dependencies { implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" - implementation 'com.github.jaiselrahman:FilePicker:1.2.0' + implementation 'com.github.jaiselrahman:FilePicker:1.2.2' // DI diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index d24ca52f07..45e1f66cd6 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -23,7 +23,6 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker @@ -34,7 +33,6 @@ import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.dialogs.DialogListItem -import im.vector.riotredesign.core.dialogs.DialogSendItemAdapter import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.ToolbarConfigurable @@ -155,15 +153,24 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { private fun setupAttachmentButton() { attachmentButton.setOnClickListener { + val intent = Intent(requireContext(), FilePickerActivity::class.java) + intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() + .setCheckPermission(true) + .setShowFiles(true) + .setShowAudios(true) + .setSkipZeroSizeFiles(true) + .build()) + startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE) + /* val items = ArrayList() // Send file items.add(DialogListItem.SendFile) // Send voice - /* + if (PreferencesManager.isSendVoiceFeatureEnabled(this)) { items.add(DialogListItem.SendVoice.INSTANCE) } - */ + // Send sticker //items.add(DialogListItem.SendSticker) @@ -182,6 +189,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } .setNegativeButton(R.string.cancel, null) .show() + */ } } @@ -189,12 +197,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { Timber.v("On send choice clicked: $dialogListItem") when (dialogListItem) { is DialogListItem.SendFile -> { - val intent = Intent(requireContext(), FilePickerActivity::class.java) - intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder() - .setCheckPermission(true) - .setSkipZeroSizeFiles(true) - .build()) - startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE) + // launchFileIntent } is DialogListItem.SendVoice -> { //launchAudioRecorderIntent() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 9ad9efa7e9..7ad0e6caae 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -21,7 +21,6 @@ import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.RiotViewModel @@ -75,25 +74,24 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, // PRIVATE METHODS ***************************************************************************** private fun handleSendMessage(action: RoomDetailActions.SendMessage) { - room.sendTextMessage(action.text, callback = object : MatrixCallback {}) + room.sendTextMessage(action.text) } private fun handleSendMedia(action: RoomDetailActions.SendMedia) { - val attachment = action.mediaFiles.firstOrNull() - ?.let { - ContentAttachmentData( - it.size, - it.duration, - it.date, - it.height, - it.width, - it.name, - it.path, - it.mimeType - ) - } - ?: return - room.sendMedia(attachment, callback = object : MatrixCallback {}) + val attachments = action.mediaFiles.map { + ContentAttachmentData( + size = it.size, + duration = it.duration, + date = it.date, + height = it.height, + width = it.width, + name = it.name, + path = it.path, + mimeType = it.mimeType, + type = ContentAttachmentData.Type.values()[it.mediaType] + ) + } + room.sendMedias(attachments) } private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { @@ -134,4 +132,5 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, timeline.dispose() super.onCleared() } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 155745dffa..74a7c284bb 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageEmoteConte import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.RiotEpoxyModel @@ -79,7 +80,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation) return when (messageContent) { - is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback) + is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) @@ -115,17 +116,24 @@ class MessageItemFactory(private val colorProvider: ColorProvider, .clickListener { view -> callback?.onMediaClicked(data, view) } } - private fun buildTextMessageItem(messageContent: MessageTextContent, + private fun buildTextMessageItem(sendState: SendState, + messageContent: MessageTextContent, informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { - val bodyToUse = messageContent.formattedBody - ?.let { - htmlRenderer.render(it) - } - ?: messageContent.body + val bodyToUse = messageContent.formattedBody?.let { + htmlRenderer.render(it) + } ?: messageContent.body - val linkifiedBody = linkifyBody(bodyToUse, callback) + val textColor = if (sendState.isSent()) { + R.color.dark_grey + } else { + R.color.brown_grey + } + val formattedBody = span(bodyToUse) { + this.textColor = colorProvider.getColor(textColor) + } + val linkifiedBody = linkifyBody(formattedBody, callback) return MessageTextItem_() .message(linkifiedBody) .informationData(informationData) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 893ab0daa0..d0d899ef50 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -25,15 +25,20 @@ import android.widget.TextView import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.riotredesign.R +import im.vector.riotredesign.features.media.MediaContentRenderer +import java.io.File object ContentUploadStateTrackerBinder { private val updateListeners = mutableMapOf() - fun bind(eventId: String, progressLayout: ViewGroup) { + fun bind(eventId: String, + mediaData: MediaContentRenderer.Data, + progressLayout: ViewGroup) { + Matrix.getInstance().currentSession?.also { session -> val uploadStateTracker = session.contentUploadProgressTracker() - val updateListener = ContentMediaProgressUpdater(progressLayout) + val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData) updateListeners[eventId] = updateListener uploadStateTracker.track(eventId, updateListener) } @@ -50,22 +55,40 @@ object ContentUploadStateTrackerBinder { } -private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup) : ContentUploadStateTracker.UpdateListener { +private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, + private val mediaData: MediaContentRenderer.Data) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { - is ContentUploadStateTracker.State.Idle, - is ContentUploadStateTracker.State.Failure, - is ContentUploadStateTracker.State.Success -> hideProgress() - is ContentUploadStateTracker.State.ProgressData -> showProgress(state) + is ContentUploadStateTracker.State.Idle -> handleIdle(state) + is ContentUploadStateTracker.State.Failure -> handleFailure(state) + is ContentUploadStateTracker.State.Success -> handleSuccess(state) + is ContentUploadStateTracker.State.ProgressData -> handleProgress(state) } } - private fun hideProgress() { - progressLayout.visibility = View.GONE + private fun handleIdle(state: ContentUploadStateTracker.State.Idle) { + if (mediaData.isLocalFile()) { + val file = File(mediaData.url) + progressLayout.visibility = View.VISIBLE + val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) + val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) + progressBar?.progress = 0 + progressTextView?.text = formatStats(progressLayout.context, 0L, file.length()) + } else { + progressLayout.visibility = View.GONE + } } - private fun showProgress(state: ContentUploadStateTracker.State.ProgressData) { + private fun handleFailure(state: ContentUploadStateTracker.State.Failure) { + + } + + private fun handleSuccess(state: ContentUploadStateTracker.State.Success) { + + } + + private fun handleProgress(state: ContentUploadStateTracker.State.ProgressData) { progressLayout.visibility = View.VISIBLE val percent = 100L * (state.current.toFloat() / state.total.toFloat()) val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt index e990e965ea..15e3d5ff08 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt @@ -37,8 +37,10 @@ abstract class MessageImageItem : AbsMessageItem() { override fun bind(holder: Holder) { super.bind(holder) MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView) - ContentUploadStateTrackerBinder.bind(eventId, holder.progressLayout) + ContentUploadStateTrackerBinder.bind(eventId, mediaData, holder.progressLayout) holder.imageView.setOnClickListener(clickListener) + holder.imageView.isEnabled = !mediaData.isLocalFile() + holder.imageView.alpha = if (mediaData.isLocalFile()) 0.5f else 1f } override fun unbind(holder: Holder) { diff --git a/vector/src/main/res/layout/item_timeline_event_image_message.xml b/vector/src/main/res/layout/item_timeline_event_image_message.xml index cb3c121a9d..21c7b12f9a 100644 --- a/vector/src/main/res/layout/item_timeline_event_image_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_image_message.xml @@ -69,11 +69,15 @@ layout="@layout/media_upload_download_progress_layout" android:layout_width="0dp" android:layout_height="46dp" + android:layout_marginStart="64dp" + android:layout_marginLeft="64dp" android:layout_marginTop="8dp" + android:layout_marginEnd="32dp" + android:layout_marginRight="32dp" android:layout_marginBottom="8dp" android:visibility="gone" - app:layout_constraintEnd_toEndOf="@+id/messageImageView" - app:layout_constraintStart_toStartOf="@+id/messageImageView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/messageImageView" tools:visibility="visible" /> diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 0225c69e7d..6a9dc6d90d 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -114,7 +114,8 @@ @drawable/vector_tabbar_background_light @drawable/pill_background_user_id_light - @drawable/pill_background_room_alias_light + @drawable/pill_background_room_alias_light + @color/riot_primary_text_color_light @android:color/white @@ -261,6 +262,11 @@ @style/Vector.TabView.Group + +