forked from GitHub-Mirror/riotX-android
Media upload: handle local echo by pinning at the bottom... will probably be changed
This commit is contained in:
parent
c9658918ed
commit
0225fc7120
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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>()
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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() {
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 ***************************************************************************
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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" />
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user