Media upload: handle local echo by pinning at the bottom... will probably be changed

This commit is contained in:
ganfra 2019-04-09 19:57:43 +02:00
parent c9658918ed
commit 0225fc7120
25 changed files with 462 additions and 152 deletions

View File

@ -22,11 +22,21 @@ import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
data class ContentAttachmentData( data class ContentAttachmentData(
val size: Long = 0, val size: Long = 0,
val duration: Long = 0, val duration: Long? = 0,
val date: Long = 0, val date: Long = 0,
val height: Long = 0, val height: Long? = 0,
val width: Long = 0, val width: Long? = 0,
val name: String? = null, val name: String? = null,
val path: String? = null, val path: String? = null,
val mimeType: String? = null val mimeType: String? = null,
) : Parcelable val type: Type
) : Parcelable {

enum class Type {
FILE,
IMAGE,
AUDIO,
VIDEO
}

}

View File

@ -22,8 +22,8 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VideoInfo( data class VideoInfo(
@Json(name = "mimetype") val mimeType: String, @Json(name = "mimetype") val mimeType: String,
@Json(name = "w") val w: Int = 0, @Json(name = "w") val width: Int = 0,
@Json(name = "h") val h: Int = 0, @Json(name = "h") val height: Int = 0,
@Json(name = "size") val size: Long = 0, @Json(name = "size") val size: Long = 0,
@Json(name = "duration") val duration: Int = 0, @Json(name = "duration") val duration: Int = 0,
@Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null,

View File

@ -16,10 +16,8 @@


package im.vector.matrix.android.api.session.room.send 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.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. * 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. * Method to send a text message asynchronously.
* @param text the text message to send * @param text the text message to send
* @param callback the callback to be notified.
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendTextMessage(text: String, callback: MatrixCallback<Event>): 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<ContentAttachmentData>): Cancelable


fun sendMedia(attachment: ContentAttachmentData, callback: MatrixCallback<Event>): Cancelable


} }

View File

@ -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
}

}

View File

@ -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.events.model.Event
import im.vector.matrix.android.api.session.room.model.RoomMember 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. * 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 root: Event,
val localId: String, val localId: String,
val displayIndex: Int, val displayIndex: Int,
val roomMember: RoomMember? val roomMember: RoomMember?,
val sendState: SendState
) { ) {


val metadata = HashMap<String, Any>() val metadata = HashMap<String, Any>()

View File

@ -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<E : RealmObject>(private val realmConfiguration: RealmConfiguration,
private val realmQueryBuilder: (Realm) -> RealmQuery<E>) {

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<RealmResults<E>> {
override fun onChange(t: RealmResults<E>) {
if (t.isNotEmpty()) {
result.removeChangeListener(this)
realm.close()
latch.countDown()
}
}
})
}
handler.post(runnable)
latch.await()
handlerThread.quit()
}


}

View File

@ -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.Event
import im.vector.matrix.android.api.session.events.model.EventType 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.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity 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.ChunkEntity
@ -89,9 +90,10 @@ internal fun ChunkEntity.add(roomId: String,
isUnlinked: Boolean = false) { isUnlinked: Boolean = false) {


assertIsManaged() assertIsManaged()
if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) { if (event.eventId.isNullOrEmpty() || this.events.fastContains(event.eventId)) {
return return
} }

var currentDisplayIndex = lastDisplayIndex(direction, 0) var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
currentDisplayIndex += 1 currentDisplayIndex += 1
@ -115,6 +117,7 @@ internal fun ChunkEntity.add(roomId: String,
this.stateIndex = currentStateIndex this.stateIndex = currentStateIndex
this.isUnlinked = isUnlinked this.isUnlinked = isUnlinked
this.displayIndex = currentDisplayIndex this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
} }
// We are not using the order of the list, but will be sorting with displayIndex field // We are not using the order of the list, but will be sorting with displayIndex field
events.add(eventEntity) events.add(eventEntity)
@ -122,14 +125,14 @@ internal fun ChunkEntity.add(roomId: String,


internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> forwardsDisplayIndex PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue } ?: defaultValue
} }


internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> forwardsStateIndex PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue } ?: defaultValue
} }

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.database.helper 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.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.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.model.RoomEntity
@ -49,6 +50,16 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
this.stateIndex = stateIndex this.stateIndex = stateIndex
this.isUnlinked = isUnlinked 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)
}

View File

@ -16,12 +16,15 @@


package im.vector.matrix.android.internal.database.model 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.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Ignore
import io.realm.annotations.Index import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import java.util.* import java.util.*
import kotlin.properties.Delegates


internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
@Index var eventId: String = "", @Index var eventId: String = "",
@ -45,6 +48,13 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI
BOTH 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 companion object


@LinkingObjects("events") @LinkingObjects("events")

View File

@ -26,6 +26,7 @@ import kotlin.properties.Delegates
internal open class RoomEntity(@PrimaryKey var roomId: String = "", internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList(), var chunks: RealmList<ChunkEntity> = RealmList(),
var untimelinedStateEvents: RealmList<EventEntity> = RealmList(), var untimelinedStateEvents: RealmList<EventEntity> = RealmList(),
var sendingTimelineEvents: RealmList<EventEntity> = RealmList(),
var areAllMembersLoaded: Boolean = false var areAllMembersLoaded: Boolean = false
) : RealmObject() { ) : RealmObject() {



View File

@ -24,22 +24,25 @@ import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy 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.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event 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.session.room.send.SendService
import im.vector.matrix.android.api.util.Cancelable 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.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.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.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.CancelableWork
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import im.vector.matrix.android.internal.util.tryTransactionAsync import im.vector.matrix.android.internal.util.tryTransactionAsync
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit


private const val SEND_WORK = "SEND_WORK" private const val SEND_WORK = "SEND_WORK"
private const val UPLOAD_WORK = "UPLOAD_WORK"
private const val BACKOFF_DELAY = 10_000L private const val BACKOFF_DELAY = 10_000L


private val WORK_CONSTRAINTS = Constraints.Builder() private val WORK_CONSTRAINTS = Constraints.Builder()
@ -48,21 +51,30 @@ private val WORK_CONSTRAINTS = Constraints.Builder()


internal class DefaultSendService(private val roomId: String, internal class DefaultSendService(private val roomId: String,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val monarchy: Monarchy) : SendService { private val monarchy: Monarchy)
: SendService {



override fun sendTextMessage(text: String): Cancelable {
override fun sendTextMessage(text: String, callback: MatrixCallback<Event>): Cancelable { val event = eventFactory.createTextEvent(roomId, text).also {
val event = eventFactory.createTextEvent(roomId, text) saveLocalEcho(it)
saveLocalEcho(event) }
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event)
WorkManager.getInstance() WorkManager.getInstance()
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, sendWork) .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
.enqueue() .enqueue()


return CancelableWork(sendWork.id) return CancelableWork(sendWork.id)
} }


override fun sendMedia(attachment: ContentAttachmentData, callback: MatrixCallback<Event>): Cancelable { override fun sendMedias(attachments: List<ContentAttachmentData>): 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 // Create an event with the media file path
val event = eventFactory.createMediaEvent(roomId, attachment).also { val event = eventFactory.createMediaEvent(roomId, attachment).also {
saveLocalEcho(it) saveLocalEcho(it)
@ -71,20 +83,28 @@ internal class DefaultSendService(private val roomId: String,
val sendWork = createSendEventWork(event) val sendWork = createSendEventWork(event)


WorkManager.getInstance() WorkManager.getInstance()
.beginUniqueWork(SEND_WORK, ExistingWorkPolicy.APPEND, uploadWork) .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
.then(sendWork) .then(sendWork)
.enqueue() .enqueue()

return CancelableWork(sendWork.id) return CancelableWork(sendWork.id)
} }


private fun saveLocalEcho(event: Event) { private fun saveLocalEcho(event: Event) {
monarchy.tryTransactionAsync { realm -> monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
?: return@tryTransactionAsync ?: return@tryTransactionAsync
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS) 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 { private fun createSendEventWork(event: Event): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)

View File

@ -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.Event
import im.vector.matrix.android.api.session.events.model.EventType 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.events.model.toContent
import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.*
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


internal class LocalEchoEventFactory(private val credentials: Credentials) { 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 { 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( val content = MessageImageContent(
type = MessageType.MSGTYPE_IMAGE, type = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image", body = attachment.name ?: "image",
info = ImageInfo( info = ImageInfo(
mimeType = attachment.mimeType ?: "image/png", mimeType = attachment.mimeType ?: "image/png",
width = attachment.width.toInt(), width = attachment.width?.toInt() ?: 0,
height = attachment.height.toInt(), height = attachment.height?.toInt() ?: 0,
size = attachment.size.toInt() size = attachment.size.toInt()
), ),
url = attachment.path url = attachment.path
@ -48,6 +54,48 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
return createEvent(roomId, content) 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 { private fun createEvent(roomId: String, content: Any? = null): Event {
return Event( return Event(
roomId = roomId, roomId = roomId,

View File

@ -20,15 +20,11 @@ import android.content.Context
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.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.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import im.vector.matrix.android.internal.util.tryTransactionSync
import org.koin.standalone.inject import org.koin.standalone.inject


internal class SendEventWorker(context: Context, params: WorkerParameters) internal class SendEventWorker(context: Context, params: WorkerParameters)
@ -42,32 +38,25 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
) )


private val roomAPI by inject<RoomAPI>() private val roomAPI by inject<RoomAPI>()
private val monarchy by inject<Monarchy>()


override fun doWork(): Result { override fun doWork(): Result {


val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()


val event = params.event val localEvent = params.event
if (event.eventId == null) { if (localEvent.eventId == null) {
return Result.failure() return Result.failure()
} }


val result = executeRequest<SendResponse> { val result = executeRequest<SendResponse> {
apiCall = roomAPI.send( apiCall = roomAPI.send(
event.eventId, localEvent.eventId,
params.roomId, params.roomId,
event.type, localEvent.type,
event.content 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() }) return result.fold({ Result.retry() }, { Result.success() })
} }
} }

View File

@ -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.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.api.util.addTo 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.*
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.query.findIncludingEvent 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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedRealmCollectionChangeListener import im.vector.matrix.android.internal.util.Debouncer
import io.realm.Realm import io.realm.*
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList




private const val INITIAL_LOAD_SIZE = 20 private const val INITIAL_LOAD_SIZE = 20
@ -68,8 +62,7 @@ internal class DefaultTimeline(
set(value) { set(value) {
field = value field = value
backgroundHandler.get()?.post { backgroundHandler.get()?.post {
val snapshot = snapshot() postSnapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
} }
} }


@ -80,41 +73,46 @@ internal class DefaultTimeline(
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private val backgroundRealm = AtomicReference<Realm>() private val backgroundRealm = AtomicReference<Realm>()
private val cancelableBag = CancelableBag() private val cancelableBag = CancelableBag()
private val debouncer = Debouncer(mainHandler)


private lateinit var liveEvents: RealmResults<EventEntity> private lateinit var liveEvents: RealmResults<EventEntity>
private var roomEntity: RoomEntity? = null


private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private val isLive = initialEventId == null private val isLive = initialEventId == null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList()) private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())

private val backwardsPaginationState = AtomicReference(PaginationState()) private val backwardsPaginationState = AtomicReference(PaginationState())
private val forwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState())



private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet -> private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
// TODO HANDLE CHANGES if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
changeSet.insertionRanges.forEach { range -> handleInitialLoad()
val (startDisplayIndex, direction) = if (range.startIndex == 0) { } else {
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) changeSet.insertionRanges.forEach { range ->
} else { val (startDisplayIndex, direction) = if (range.startIndex == 0) {
Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
} } else {
val state = getPaginationState(direction) Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
if (state.isPaginating) { }
// We are getting new items from pagination val state = getPaginationState(direction)
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount) if (state.isPaginating) {
if (shouldPostSnapshot) { // 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() 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) { override fun paginate(direction: Timeline.Direction, count: Int) {
backgroundHandler.get()?.post { backgroundHandler.get()?.post {
@ -142,12 +140,19 @@ internal class DefaultTimeline(
val realm = Realm.getInstance(realmConfiguration) val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm) backgroundRealm.set(realm)
clearUnlinkedEvents(realm) clearUnlinkedEvents(realm)
isReady.set(true)
roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()?.also {
it.sendingTimelineEvents.addChangeListener { _ ->
postSnapshot()
}
}

liveEvents = buildEventQuery(realm) liveEvents = buildEventQuery(realm)
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll() .findAllAsync()
.also { it.addChangeListener(eventsChangeListener) } .also { it.addChangeListener(eventsChangeListener) }
handleInitialLoad()
isReady.set(true)
} }
} }
} }
@ -171,7 +176,7 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }


// Private methods ***************************************************************************** // Private methods *****************************************************************************


private fun hasMoreInCache(direction: Timeline.Direction): Boolean { private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
val localRealm = Realm.getInstance(realmConfiguration) 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 * 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, private fun paginateInternal(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
@ -222,8 +227,19 @@ internal class DefaultTimeline(
return !shouldFetchMore return !shouldFetchMore
} }


private fun snapshot(): List<TimelineEvent> { private fun createSnapshot(): List<TimelineEvent> {
return builtEvents.toList() return buildSendingEvents() + builtEvents.toList()
}

private fun buildSendingEvents(): List<TimelineEvent> {
val sendingEvents = ArrayList<TimelineEvent>()
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 { private fun canPaginate(direction: Timeline.Direction): Boolean {
@ -282,9 +298,9 @@ internal class DefaultTimeline(
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val token = getTokenLive(direction) ?: return val token = getTokenLive(direction) ?: return
val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = token, from = token,
direction = direction.toPaginationDirection(), direction = direction.toPaginationDirection(),
limit = limit) limit = limit)


Timber.v("Should fetch $limit items $direction") Timber.v("Should fetch $limit items $direction")
paginationTask.configureWith(params) paginationTask.configureWith(params)
@ -414,8 +430,9 @@ internal class DefaultTimeline(
} }


private fun postSnapshot() { private fun postSnapshot() {
val snapshot = snapshot() val snapshot = createSnapshot()
mainHandler.post { listener?.onUpdated(snapshot) } val runnable = Runnable { listener?.onUpdated(snapshot) }
debouncer.debounce("post_snapshot", runnable, 50)
} }


// Extension methods *************************************************************************** // Extension methods ***************************************************************************

View File

@ -29,7 +29,8 @@ internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberE
eventEntity.asDomain(), eventEntity.asDomain(),
eventEntity.localId, eventEntity.localId,
eventEntity.displayIndex, eventEntity.displayIndex,
roomMember roomMember,
eventEntity.sendState
) )
} }



View File

@ -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.helper.lastStateIndex
import im.vector.matrix.android.internal.database.model.ChunkEntity 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.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.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
@ -108,6 +109,15 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
timelineStateOffset timelineStateOffset
) )
roomEntity.addOrUpdate(chunkEntity) 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) roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)


@ -161,7 +171,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
} else { } else {
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken } realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
} }

lastChunk?.isLastForward = false lastChunk?.isLastForward = false
chunkEntity.isLastForward = true chunkEntity.isLastForward = true
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)

View File

@ -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<String, Runnable>()

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)
}
}

View File

@ -209,7 +209,7 @@ dependencies {
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$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 // DI

View File

@ -23,7 +23,6 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker 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.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.DialogListItem 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.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
@ -155,15 +153,24 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {


private fun setupAttachmentButton() { private fun setupAttachmentButton() {
attachmentButton.setOnClickListener { 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<DialogListItem>() val items = ArrayList<DialogListItem>()
// Send file // Send file
items.add(DialogListItem.SendFile) items.add(DialogListItem.SendFile)
// Send voice // Send voice
/*
if (PreferencesManager.isSendVoiceFeatureEnabled(this)) { if (PreferencesManager.isSendVoiceFeatureEnabled(this)) {
items.add(DialogListItem.SendVoice.INSTANCE) items.add(DialogListItem.SendVoice.INSTANCE)
} }
*/


// Send sticker // Send sticker
//items.add(DialogListItem.SendSticker) //items.add(DialogListItem.SendSticker)
@ -182,6 +189,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
*/
} }
} }


@ -189,12 +197,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
Timber.v("On send choice clicked: $dialogListItem") Timber.v("On send choice clicked: $dialogListItem")
when (dialogListItem) { when (dialogListItem) {
is DialogListItem.SendFile -> { is DialogListItem.SendFile -> {
val intent = Intent(requireContext(), FilePickerActivity::class.java) // launchFileIntent
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
.setCheckPermission(true)
.setSkipZeroSizeFiles(true)
.build())
startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
} }
is DialogListItem.SendVoice -> { is DialogListItem.SendVoice -> {
//launchAudioRecorderIntent() //launchAudioRecorderIntent()

View File

@ -21,7 +21,6 @@ import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session 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.android.api.session.content.ContentAttachmentData
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
@ -75,25 +74,24 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************


private fun handleSendMessage(action: RoomDetailActions.SendMessage) { private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {}) room.sendTextMessage(action.text)
} }


private fun handleSendMedia(action: RoomDetailActions.SendMedia) { private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
val attachment = action.mediaFiles.firstOrNull() val attachments = action.mediaFiles.map {
?.let { ContentAttachmentData(
ContentAttachmentData( size = it.size,
it.size, duration = it.duration,
it.duration, date = it.date,
it.date, height = it.height,
it.height, width = it.width,
it.width, name = it.name,
it.name, path = it.path,
it.path, mimeType = it.mimeType,
it.mimeType type = ContentAttachmentData.Type.values()[it.mediaType]
) )
} }
?: return room.sendMedias(attachments)
room.sendMedia(attachment, callback = object : MatrixCallback<Event> {})
} }


private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
@ -134,4 +132,5 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
timeline.dispose() timeline.dispose()
super.onCleared() super.onCleared()
} }

} }

View File

@ -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.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent 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.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.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
@ -79,7 +80,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation) val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation)


return when (messageContent) { 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 MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback)
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(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) } .clickListener { view -> callback?.onMediaClicked(data, view) }
} }


private fun buildTextMessageItem(messageContent: MessageTextContent, private fun buildTextMessageItem(sendState: SendState,
messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {


val bodyToUse = messageContent.formattedBody val bodyToUse = messageContent.formattedBody?.let {
?.let { htmlRenderer.render(it)
htmlRenderer.render(it) } ?: messageContent.body
}
?: 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_() return MessageTextItem_()
.message(linkifiedBody) .message(linkifiedBody)
.informationData(informationData) .informationData(informationData)

View File

@ -25,15 +25,20 @@ import android.widget.TextView
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.media.MediaContentRenderer
import java.io.File


object ContentUploadStateTrackerBinder { object ContentUploadStateTrackerBinder {


private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>() private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()


fun bind(eventId: String, progressLayout: ViewGroup) { fun bind(eventId: String,
mediaData: MediaContentRenderer.Data,
progressLayout: ViewGroup) {

Matrix.getInstance().currentSession?.also { session -> Matrix.getInstance().currentSession?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker() val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout) val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData)
updateListeners[eventId] = updateListener updateListeners[eventId] = updateListener
uploadStateTracker.track(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) { override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) { when (state) {
is ContentUploadStateTracker.State.Idle, is ContentUploadStateTracker.State.Idle -> handleIdle(state)
is ContentUploadStateTracker.State.Failure, is ContentUploadStateTracker.State.Failure -> handleFailure(state)
is ContentUploadStateTracker.State.Success -> hideProgress() is ContentUploadStateTracker.State.Success -> handleSuccess(state)
is ContentUploadStateTracker.State.ProgressData -> showProgress(state) is ContentUploadStateTracker.State.ProgressData -> handleProgress(state)
} }
} }


private fun hideProgress() { private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
progressLayout.visibility = View.GONE if (mediaData.isLocalFile()) {
val file = File(mediaData.url)
progressLayout.visibility = View.VISIBLE
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(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 progressLayout.visibility = View.VISIBLE
val percent = 100L * (state.current.toFloat() / state.total.toFloat()) val percent = 100L * (state.current.toFloat() / state.total.toFloat())
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar) val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)

View File

@ -37,8 +37,10 @@ abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView) 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.setOnClickListener(clickListener)
holder.imageView.isEnabled = !mediaData.isLocalFile()
holder.imageView.alpha = if (mediaData.isLocalFile()) 0.5f else 1f
} }


override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {

View File

@ -69,11 +69,15 @@
layout="@layout/media_upload_download_progress_layout" layout="@layout/media_upload_download_progress_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="46dp" android:layout_height="46dp"
android:layout_marginStart="64dp"
android:layout_marginLeft="64dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="@+id/messageImageView" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/messageImageView" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageImageView" app:layout_constraintTop_toBottomOf="@+id/messageImageView"
tools:visibility="visible" /> tools:visibility="visible" />



View File

@ -114,7 +114,8 @@
<item name="vctr_tabbar_background">@drawable/vector_tabbar_background_light</item> <item name="vctr_tabbar_background">@drawable/vector_tabbar_background_light</item>


<item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_light</item> <item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_light</item>
<item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_light</item> <item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_light
</item>


<item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_light</item> <item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_light</item>
<item name="vctr_pill_text_color_room_alias">@android:color/white</item> <item name="vctr_pill_text_color_room_alias">@android:color/white</item>
@ -261,6 +262,11 @@
<item name="actionBarTabStyle">@style/Vector.TabView.Group</item> <item name="actionBarTabStyle">@style/Vector.TabView.Group</item>
</style> </style>


<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat">
<item name="titleTextColor">?attr/actionMenuTextColor</item>
<item name="android:background">?attr/colorPrimary</item>
</style>

<style name="AppTheme.Dialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert" /> <style name="AppTheme.Dialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert" />


</resources> </resources>