mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Merge pull request #484 from vector-im/feature/timeline_read_receipts
Feature/timeline read receipts
This commit is contained in:
commit
bc1350aaf5
60 changed files with 1366 additions and 351 deletions
|
@ -2,7 +2,7 @@ Changes in RiotX 0.4.0 (2019-XX-XX)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
-
|
- Display read receipts in timeline (#81)
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
-
|
-
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.rx
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
@ -49,6 +50,10 @@ class RxRoom(private val room: Room) {
|
||||||
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
|
||||||
|
return room.getEventReadReceiptsLive(eventId).asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Room.rx(): RxRoom {
|
fun Room.rx(): RxRoom {
|
||||||
|
|
|
@ -16,8 +16,9 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model
|
package im.vector.matrix.android.api.session.room.model
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
|
|
||||||
data class ReadReceipt(
|
data class ReadReceipt(
|
||||||
val userId: String,
|
val user: User,
|
||||||
val eventId: String,
|
|
||||||
val originServerTs: Long
|
val originServerTs: Long
|
||||||
)
|
)
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.read
|
package im.vector.matrix.android.api.session.room.read
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
||||||
|
@ -39,4 +41,6 @@ interface ReadService {
|
||||||
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
|
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun isEventRead(eventId: String): Boolean
|
fun isEventRead(eventId: String): Boolean
|
||||||
|
|
||||||
|
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||||
}
|
}
|
|
@ -20,6 +20,7 @@ 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.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
@ -37,7 +38,8 @@ data class TimelineEvent(
|
||||||
val senderName: String?,
|
val senderName: String?,
|
||||||
val isUniqueDisplayName: Boolean,
|
val isUniqueDisplayName: Boolean,
|
||||||
val senderAvatar: String?,
|
val senderAvatar: String?,
|
||||||
val annotations: EventAnnotationsSummary? = null
|
val annotations: EventAnnotationsSummary? = null,
|
||||||
|
val readReceipts: List<ReadReceipt> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val metadata = HashMap<String, Any>()
|
val metadata = HashMap<String, Any>()
|
||||||
|
@ -65,8 +67,8 @@ data class TimelineEvent(
|
||||||
"$name (${root.senderId})"
|
"$name (${root.senderId})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: root.senderId
|
?: root.senderId
|
||||||
?: ""
|
?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,7 +96,7 @@ fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
|
||||||
* Get last MessageContent, after a possible edition
|
* Get last MessageContent, after a possible edition
|
||||||
*/
|
*/
|
||||||
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
?: root.getClearContent().toModel()
|
?: root.getClearContent().toModel()
|
||||||
|
|
||||||
|
|
||||||
fun TimelineEvent.getTextEditableContent(): String? {
|
fun TimelineEvent.getTextEditableContent(): String? {
|
||||||
|
|
|
@ -25,12 +25,12 @@ interface TimelineService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
|
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
|
||||||
* You can filter the type you want to grab with the allowedTypes param.
|
* You can also configure some settings with the [settings] param.
|
||||||
* @param eventId the optional initial eventId.
|
* @param eventId the optional initial eventId.
|
||||||
* @param allowedTypes the optional filter types
|
* @param settings settings to configure the timeline.
|
||||||
* @return the instantiated timeline
|
* @return the instantiated timeline
|
||||||
*/
|
*/
|
||||||
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
|
||||||
|
|
||||||
|
|
||||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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.timeline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data class holding setting values for a [Timeline] instance.
|
||||||
|
*/
|
||||||
|
data class TimelineSettings(
|
||||||
|
/**
|
||||||
|
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
|
||||||
|
*/
|
||||||
|
val initialSize: Int,
|
||||||
|
/**
|
||||||
|
* A flag to filter edit events
|
||||||
|
*/
|
||||||
|
val filterEdits: Boolean = false,
|
||||||
|
/**
|
||||||
|
* A flag to filter by types. It should be used with [allowedTypes] field
|
||||||
|
*/
|
||||||
|
val filterTypes: Boolean = false,
|
||||||
|
/**
|
||||||
|
* If [filterTypes] is true, the list of types allowed by the list.
|
||||||
|
*/
|
||||||
|
val allowedTypes: List<String> = emptyList(),
|
||||||
|
/**
|
||||||
|
* If true, will build read receipts for each event.
|
||||||
|
*/
|
||||||
|
val buildReadReceipts: Boolean = true
|
||||||
|
|
||||||
|
)
|
|
@ -23,9 +23,12 @@ 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
|
||||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
|
@ -133,6 +136,28 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
val localId = TimelineEventEntity.nextId(realm)
|
val localId = TimelineEventEntity.nextId(realm)
|
||||||
|
val eventId = event.eventId ?: ""
|
||||||
|
val senderId = event.senderId ?: ""
|
||||||
|
|
||||||
|
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
?: ReadReceiptsSummaryEntity(eventId, roomId)
|
||||||
|
|
||||||
|
// Update RR for the sender of a new message with a dummy one
|
||||||
|
|
||||||
|
if (event.originServerTs != null) {
|
||||||
|
val timestampOfEvent = event.originServerTs.toDouble()
|
||||||
|
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
|
||||||
|
// If the synced RR is older, update
|
||||||
|
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
|
||||||
|
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
|
||||||
|
readReceiptOfSender.eventId = eventId
|
||||||
|
readReceiptOfSender.originServerTs = timestampOfEvent
|
||||||
|
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
|
||||||
|
readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val eventEntity = TimelineEventEntity(localId).also {
|
val eventEntity = TimelineEventEntity(localId).also {
|
||||||
it.root = event.toEntity(roomId).apply {
|
it.root = event.toEntity(roomId).apply {
|
||||||
this.stateIndex = currentStateIndex
|
this.stateIndex = currentStateIndex
|
||||||
|
@ -140,9 +165,10 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
this.displayIndex = currentDisplayIndex
|
this.displayIndex = currentDisplayIndex
|
||||||
this.sendState = SendState.SYNCED
|
this.sendState = SendState.SYNCED
|
||||||
}
|
}
|
||||||
it.eventId = event.eventId ?: ""
|
it.eventId = eventId
|
||||||
it.roomId = roomId
|
it.roomId = roomId
|
||||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, it.eventId).findFirst()
|
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
it.readReceipts = readReceiptsSummaryEntity
|
||||||
}
|
}
|
||||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||||
timelineEvents.add(position, eventEntity)
|
timelineEvents.add(position, eventEntity)
|
||||||
|
@ -150,14 +176,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
|
||||||
}
|
}
|
|
@ -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.database.mapper
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {
|
||||||
|
|
||||||
|
fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
|
||||||
|
if (readReceiptsSummaryEntity == null) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
|
val readReceipts = readReceiptsSummaryEntity.readReceipts
|
||||||
|
readReceipts
|
||||||
|
.mapNotNull {
|
||||||
|
val user = UserEntity.where(realm, it.userId).findFirst()
|
||||||
|
?: return@mapNotNull null
|
||||||
|
ReadReceipt(user.asDomain(), it.originServerTs.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,7 +26,8 @@ import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RoomSummaryMapper @Inject constructor(
|
internal class RoomSummaryMapper @Inject constructor(
|
||||||
val cryptoService: CryptoService
|
val cryptoService: CryptoService,
|
||||||
|
val timelineEventMapper: TimelineEventMapper
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
||||||
|
@ -34,7 +35,9 @@ internal class RoomSummaryMapper @Inject constructor(
|
||||||
RoomTag(it.tagName, it.tagOrder)
|
RoomTag(it.tagName, it.tagOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
val latestEvent = roomSummaryEntity.latestEvent?.asDomain()
|
val latestEvent = roomSummaryEntity.latestEvent?.let {
|
||||||
|
timelineEventMapper.map(it)
|
||||||
|
}
|
||||||
if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) {
|
if (latestEvent?.root?.isEncrypted() == true && latestEvent.root.mxDecryptionResult == null) {
|
||||||
//TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
//TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||||
//for now decrypt sync
|
//for now decrypt sync
|
||||||
|
|
|
@ -17,29 +17,38 @@
|
||||||
package im.vector.matrix.android.internal.database.mapper
|
package im.vector.matrix.android.internal.database.mapper
|
||||||
|
|
||||||
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.ReadReceipt
|
||||||
|
|
||||||
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.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal object TimelineEventMapper {
|
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
|
||||||
|
|
||||||
fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent {
|
|
||||||
|
|
||||||
|
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
|
||||||
|
val readReceipts = if (buildReadReceipts) {
|
||||||
|
correctedReadReceipts ?: timelineEventEntity.readReceipts
|
||||||
|
?.let {
|
||||||
|
readReceiptsSummaryMapper.map(it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
return TimelineEvent(
|
return TimelineEvent(
|
||||||
root = timelineEventEntity.root?.asDomain()
|
root = timelineEventEntity.root?.asDomain()
|
||||||
?: Event("", timelineEventEntity.eventId),
|
?: Event("", timelineEventEntity.eventId),
|
||||||
annotations = timelineEventEntity.annotations?.asDomain(),
|
annotations = timelineEventEntity.annotations?.asDomain(),
|
||||||
localId = timelineEventEntity.localId,
|
localId = timelineEventEntity.localId,
|
||||||
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
||||||
senderName = timelineEventEntity.senderName,
|
senderName = timelineEventEntity.senderName,
|
||||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||||
senderAvatar = timelineEventEntity.senderAvatar
|
senderAvatar = timelineEventEntity.senderAvatar,
|
||||||
|
readReceipts = readReceipts?.sortedByDescending {
|
||||||
|
it.originServerTs
|
||||||
|
} ?: emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun TimelineEventEntity.asDomain(): TimelineEvent {
|
|
||||||
return TimelineEventMapper.map(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,18 @@
|
||||||
package im.vector.matrix.android.internal.database.model
|
package im.vector.matrix.android.internal.database.model
|
||||||
|
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
|
import io.realm.RealmResults
|
||||||
|
import io.realm.annotations.LinkingObjects
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "",
|
internal open class ReadReceiptEntity(@PrimaryKey var primaryKey: String = "",
|
||||||
var userId: String = "",
|
var eventId: String = "",
|
||||||
var eventId: String = "",
|
var roomId: String = "",
|
||||||
var roomId: String = "",
|
var userId: String = "",
|
||||||
var originServerTs: Double = 0.0
|
var originServerTs: Double = 0.0
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
companion object
|
companion object
|
||||||
|
|
||||||
|
@LinkingObjects("readReceipts")
|
||||||
|
val summary: RealmResults<ReadReceiptsSummaryEntity>? = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import io.realm.RealmList
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.RealmResults
|
||||||
|
import io.realm.annotations.LinkingObjects
|
||||||
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
|
internal open class ReadReceiptsSummaryEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
var eventId: String = "",
|
||||||
|
var roomId: String = "",
|
||||||
|
var readReceipts: RealmList<ReadReceiptEntity> = RealmList()
|
||||||
|
) : RealmObject() {
|
||||||
|
|
||||||
|
@LinkingObjects("readReceipts")
|
||||||
|
val timelineEvent: RealmResults<TimelineEventEntity>? = null
|
||||||
|
|
||||||
|
companion object
|
||||||
|
|
||||||
|
}
|
|
@ -22,26 +22,27 @@ import io.realm.annotations.RealmModule
|
||||||
* Realm module for Session
|
* Realm module for Session
|
||||||
*/
|
*/
|
||||||
@RealmModule(library = true,
|
@RealmModule(library = true,
|
||||||
classes = [
|
classes = [
|
||||||
ChunkEntity::class,
|
ChunkEntity::class,
|
||||||
EventEntity::class,
|
EventEntity::class,
|
||||||
TimelineEventEntity::class,
|
TimelineEventEntity::class,
|
||||||
FilterEntity::class,
|
FilterEntity::class,
|
||||||
GroupEntity::class,
|
GroupEntity::class,
|
||||||
GroupSummaryEntity::class,
|
GroupSummaryEntity::class,
|
||||||
ReadReceiptEntity::class,
|
ReadReceiptEntity::class,
|
||||||
RoomEntity::class,
|
RoomEntity::class,
|
||||||
RoomSummaryEntity::class,
|
RoomSummaryEntity::class,
|
||||||
RoomTagEntity::class,
|
RoomTagEntity::class,
|
||||||
SyncEntity::class,
|
SyncEntity::class,
|
||||||
UserEntity::class,
|
UserEntity::class,
|
||||||
EventAnnotationsSummaryEntity::class,
|
EventAnnotationsSummaryEntity::class,
|
||||||
ReactionAggregatedSummaryEntity::class,
|
ReactionAggregatedSummaryEntity::class,
|
||||||
EditAggregatedSummaryEntity::class,
|
EditAggregatedSummaryEntity::class,
|
||||||
PushRulesEntity::class,
|
PushRulesEntity::class,
|
||||||
PushRuleEntity::class,
|
PushRuleEntity::class,
|
||||||
PushConditionEntity::class,
|
PushConditionEntity::class,
|
||||||
PusherEntity::class,
|
PusherEntity::class,
|
||||||
PusherDataEntity::class
|
PusherDataEntity::class,
|
||||||
])
|
ReadReceiptsSummaryEntity::class
|
||||||
|
])
|
||||||
internal class SessionRealmModule
|
internal class SessionRealmModule
|
||||||
|
|
|
@ -30,7 +30,8 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
||||||
var senderName: String? = null,
|
var senderName: String? = null,
|
||||||
var isUniqueDisplayName: Boolean = false,
|
var isUniqueDisplayName: Boolean = false,
|
||||||
var senderAvatar: String? = null,
|
var senderAvatar: String? = null,
|
||||||
var senderMembershipEvent: EventEntity? = null
|
var senderMembershipEvent: EventEntity? = null,
|
||||||
|
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@LinkingObjects("timelineEvents")
|
@LinkingObjects("timelineEvents")
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.query
|
||||||
|
|
||||||
|
internal object FilterContent {
|
||||||
|
|
||||||
|
internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
||||||
|
|
||||||
|
}
|
|
@ -26,4 +26,22 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use
|
||||||
return realm.where<ReadReceiptEntity>()
|
return realm.where<ReadReceiptEntity>()
|
||||||
.equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
|
.equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
|
||||||
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
|
.equalTo(ReadReceiptEntityFields.USER_ID, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
|
||||||
|
return ReadReceiptEntity().apply {
|
||||||
|
this.primaryKey = "${roomId}_$userId"
|
||||||
|
this.eventId = eventId
|
||||||
|
this.roomId = roomId
|
||||||
|
this.userId = userId
|
||||||
|
this.originServerTs = originServerTs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
|
||||||
|
return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
|
||||||
|
?: realm.createObject(ReadReceiptEntity::class.java, "${roomId}_$userId").apply {
|
||||||
|
this.roomId = roomId
|
||||||
|
this.userId = userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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.query
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
|
internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<ReadReceiptsSummaryEntity> {
|
||||||
|
return realm.where<ReadReceiptsSummaryEntity>()
|
||||||
|
.equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery<ReadReceiptsSummaryEntity> {
|
||||||
|
val query = realm.where<ReadReceiptsSummaryEntity>()
|
||||||
|
if (roomId != null) {
|
||||||
|
query.equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
|
@ -21,7 +21,9 @@ import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||||
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
||||||
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
|
||||||
|
@ -47,6 +49,8 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val roomSummaryMapper: RoomSummaryMapper,
|
private val roomSummaryMapper: RoomSummaryMapper,
|
||||||
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
|
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
private val inviteTask: InviteTask,
|
private val inviteTask: InviteTask,
|
||||||
|
@ -61,13 +65,29 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
||||||
private val leaveRoomTask: LeaveRoomTask) {
|
private val leaveRoomTask: LeaveRoomTask) {
|
||||||
|
|
||||||
fun create(roomId: String): Room {
|
fun create(roomId: String): Room {
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, cryptoService, paginationTask)
|
val timelineService = DefaultTimelineService(roomId,
|
||||||
|
monarchy,
|
||||||
|
taskExecutor,
|
||||||
|
contextOfEventTask,
|
||||||
|
cryptoService,
|
||||||
|
paginationTask,
|
||||||
|
timelineEventMapper,
|
||||||
|
readReceiptsSummaryMapper
|
||||||
|
)
|
||||||
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
|
val sendService = DefaultSendService(context, credentials, roomId, eventFactory, cryptoService, monarchy)
|
||||||
val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask)
|
val stateService = DefaultStateService(roomId, monarchy.realmConfiguration, taskExecutor, sendStateTask)
|
||||||
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
||||||
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
|
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, readReceiptsSummaryMapper, credentials)
|
||||||
val relationService = DefaultRelationService(context,
|
val relationService = DefaultRelationService(context,
|
||||||
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor)
|
credentials,
|
||||||
|
roomId,
|
||||||
|
eventFactory,
|
||||||
|
cryptoService,
|
||||||
|
findReactionEventForUndoTask,
|
||||||
|
fetchEditHistoryTask,
|
||||||
|
monarchy,
|
||||||
|
taskExecutor
|
||||||
|
)
|
||||||
|
|
||||||
return DefaultRoom(
|
return DefaultRoom(
|
||||||
roomId,
|
roomId,
|
||||||
|
|
|
@ -16,12 +16,21 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.read
|
package im.vector.matrix.android.internal.session.room.read
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||||
|
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
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.EventAnnotationsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
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
|
||||||
|
@ -33,6 +42,7 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val setReadMarkersTask: SetReadMarkersTask,
|
private val setReadMarkersTask: SetReadMarkersTask,
|
||||||
|
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||||
private val credentials: Credentials) : ReadService {
|
private val credentials: Credentials) : ReadService {
|
||||||
|
|
||||||
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
|
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
|
||||||
|
@ -67,16 +77,28 @@ internal class DefaultReadService @Inject constructor(private val roomId: String
|
||||||
var isEventRead = false
|
var isEventRead = false
|
||||||
monarchy.doWithRealm {
|
monarchy.doWithRealm {
|
||||||
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
||||||
?: return@doWithRealm
|
?: return@doWithRealm
|
||||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
||||||
?: return@doWithRealm
|
?: return@doWithRealm
|
||||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||||
?: Int.MIN_VALUE
|
?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
||||||
?: Int.MAX_VALUE
|
?: Int.MAX_VALUE
|
||||||
isEventRead = eventToCheckIndex <= readReceiptIndex
|
isEventRead = eventToCheckIndex <= readReceiptIndex
|
||||||
}
|
}
|
||||||
return isEventRead
|
return isEventRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
|
||||||
|
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||||
|
ReadReceiptsSummaryEntity.where(realm, eventId)
|
||||||
|
}
|
||||||
|
return Transformations.map(liveEntity) { realmResults ->
|
||||||
|
realmResults.firstOrNull()?.let {
|
||||||
|
readReceiptsSummaryMapper.map(it)
|
||||||
|
}?.sortedByDescending {
|
||||||
|
it.originServerTs
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -19,21 +19,41 @@ package im.vector.matrix.android.internal.session.room.timeline
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
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.model.ReadReceipt
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
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.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||||
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.model.*
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.*
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.query.FilterContent
|
||||||
|
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
|
||||||
|
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||||
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.database.query.whereInRoom
|
||||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
import im.vector.matrix.android.internal.task.TaskConstraints
|
||||||
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 im.vector.matrix.android.internal.util.Debouncer
|
import im.vector.matrix.android.internal.util.Debouncer
|
||||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||||
import im.vector.matrix.android.internal.util.createUIHandler
|
import im.vector.matrix.android.internal.util.createUIHandler
|
||||||
import io.realm.*
|
import io.realm.OrderedCollectionChangeSet
|
||||||
|
import io.realm.OrderedRealmCollectionChangeListener
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.RealmResults
|
||||||
|
import io.realm.Sort
|
||||||
import 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
|
||||||
|
@ -42,7 +62,6 @@ import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
|
||||||
private const val INITIAL_LOAD_SIZE = 30
|
|
||||||
private const val MIN_FETCHING_COUNT = 30
|
private const val MIN_FETCHING_COUNT = 30
|
||||||
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
||||||
|
|
||||||
|
@ -53,9 +72,11 @@ internal class DefaultTimeline(
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val allowedTypes: List<String>?
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
) : Timeline {
|
private val settings: TimelineSettings,
|
||||||
|
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
||||||
|
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||||
|
@ -77,6 +98,8 @@ internal class DefaultTimeline(
|
||||||
private val debouncer = Debouncer(mainHandler)
|
private val debouncer = Debouncer(mainHandler)
|
||||||
|
|
||||||
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
|
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
|
||||||
|
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
|
||||||
|
|
||||||
private var roomEntity: RoomEntity? = null
|
private var roomEntity: RoomEntity? = null
|
||||||
|
|
||||||
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||||
|
@ -87,11 +110,8 @@ internal class DefaultTimeline(
|
||||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||||
private val forwardsPaginationState = AtomicReference(PaginationState())
|
private val forwardsPaginationState = AtomicReference(PaginationState())
|
||||||
|
|
||||||
|
|
||||||
private val timelineID = UUID.randomUUID().toString()
|
private val timelineID = UUID.randomUUID().toString()
|
||||||
|
|
||||||
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
|
|
||||||
|
|
||||||
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
|
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
|
||||||
|
|
||||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
|
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
|
||||||
|
@ -130,9 +150,9 @@ internal class DefaultTimeline(
|
||||||
val eventEntity = results[index]
|
val eventEntity = results[index]
|
||||||
eventEntity?.eventId?.let { eventId ->
|
eventEntity?.eventId?.let { eventId ->
|
||||||
builtEventsIdMap[eventId]?.let { builtIndex ->
|
builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||||
//Update the relation of existing event
|
//Update an existing event
|
||||||
builtEvents[builtIndex]?.let { te ->
|
builtEvents[builtIndex]?.let { te ->
|
||||||
builtEvents[builtIndex] = eventEntity.asDomain()
|
builtEvents[builtIndex] = buildTimelineEvent(eventEntity)
|
||||||
hasChanged = true
|
hasChanged = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,34 +182,8 @@ internal class DefaultTimeline(
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// private val newSessionListener = object : NewSessionListener {
|
|
||||||
// override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
|
||||||
// if (roomId == this@DefaultTimeline.roomId) {
|
|
||||||
// Timber.v("New session id detected for this room")
|
|
||||||
// BACKGROUND_HANDLER.post {
|
|
||||||
// val realm = backgroundRealm.get()
|
|
||||||
// var hasChange = false
|
|
||||||
// builtEvents.forEachIndexed { index, timelineEvent ->
|
|
||||||
// if (timelineEvent.isEncrypted()) {
|
|
||||||
// val eventContent = timelineEvent.root.content.toModel<EncryptedEventContent>()
|
|
||||||
// if (eventContent?.sessionId == sessionId
|
|
||||||
// && (timelineEvent.root.mClearEvent == null || timelineEvent.root.mCryptoError != null)) {
|
|
||||||
// //we need to rebuild this event
|
|
||||||
// EventEntity.where(realm, eventId = timelineEvent.root.eventId!!).findFirst()?.let {
|
|
||||||
// //builtEvents[index] = timelineEventFactory.create(it, realm)
|
|
||||||
// hasChange = true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (hasChange) postSnapshot()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Public methods ******************************************************************************
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
BACKGROUND_HANDLER.post {
|
BACKGROUND_HANDLER.post {
|
||||||
|
@ -234,15 +228,20 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
liveEvents = buildEventQuery(realm)
|
liveEvents = buildEventQuery(realm)
|
||||||
|
.filterEventsWithSettings()
|
||||||
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
.findAllAsync()
|
.findAllAsync()
|
||||||
.also { it.addChangeListener(eventsChangeListener) }
|
.also { it.addChangeListener(eventsChangeListener) }
|
||||||
|
|
||||||
isReady.set(true)
|
|
||||||
|
|
||||||
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
|
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
|
||||||
.findAllAsync()
|
.findAllAsync()
|
||||||
.also { it.addChangeListener(relationsListener) }
|
.also { it.addChangeListener(relationsListener) }
|
||||||
|
|
||||||
|
if (settings.buildReadReceipts) {
|
||||||
|
hiddenReadReceipts.start(realm, liveEvents, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
isReady.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,6 +255,9 @@ internal class DefaultTimeline(
|
||||||
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
||||||
eventRelations.removeAllChangeListeners()
|
eventRelations.removeAllChangeListeners()
|
||||||
liveEvents.removeAllChangeListeners()
|
liveEvents.removeAllChangeListeners()
|
||||||
|
if (settings.buildReadReceipts) {
|
||||||
|
hiddenReadReceipts.dispose()
|
||||||
|
}
|
||||||
backgroundRealm.getAndSet(null).also {
|
backgroundRealm.getAndSet(null).also {
|
||||||
it.close()
|
it.close()
|
||||||
}
|
}
|
||||||
|
@ -267,12 +269,28 @@ internal class DefaultTimeline(
|
||||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TimelineHiddenReadReceipts.Delegate
|
||||||
|
|
||||||
|
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||||
|
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||||
|
//Update the relation of existing event
|
||||||
|
builtEvents[builtIndex]?.let { te ->
|
||||||
|
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReadReceiptsUpdated() {
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
// Private methods *****************************************************************************
|
// Private methods *****************************************************************************
|
||||||
|
|
||||||
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||||
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
||||||
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
|
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
|
||||||
?: return false
|
?: return false
|
||||||
if (direction == Timeline.Direction.FORWARDS) {
|
if (direction == Timeline.Direction.FORWARDS) {
|
||||||
if (findCurrentChunk(localRealm)?.isLastForward == true) {
|
if (findCurrentChunk(localRealm)?.isLastForward == true) {
|
||||||
return false
|
return false
|
||||||
|
@ -329,9 +347,11 @@ internal class DefaultTimeline(
|
||||||
val sendingEvents = ArrayList<TimelineEvent>()
|
val sendingEvents = ArrayList<TimelineEvent>()
|
||||||
if (hasReachedEnd(Timeline.Direction.FORWARDS)) {
|
if (hasReachedEnd(Timeline.Direction.FORWARDS)) {
|
||||||
roomEntity?.sendingTimelineEvents
|
roomEntity?.sendingTimelineEvents
|
||||||
?.filter { allowedTypes?.contains(it.root?.type) ?: false }
|
?.where()
|
||||||
|
?.filterEventsWithSettings()
|
||||||
|
?.findAll()
|
||||||
?.forEach {
|
?.forEach {
|
||||||
sendingEvents.add(it.asDomain())
|
sendingEvents.add(timelineEventMapper.map(it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sendingEvents
|
return sendingEvents
|
||||||
|
@ -378,7 +398,7 @@ internal class DefaultTimeline(
|
||||||
if (initialEventId != null && shouldFetchInitialEvent) {
|
if (initialEventId != null && shouldFetchInitialEvent) {
|
||||||
fetchEvent(initialEventId)
|
fetchEvent(initialEventId)
|
||||||
} else {
|
} else {
|
||||||
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
|
val count = Math.min(settings.initialSize, liveEvents.size)
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
||||||
} else {
|
} else {
|
||||||
|
@ -395,9 +415,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")
|
||||||
cancelableBag += paginationTask
|
cancelableBag += paginationTask
|
||||||
|
@ -463,10 +483,11 @@ internal class DefaultTimeline(
|
||||||
nextDisplayIndex = offsetIndex + 1
|
nextDisplayIndex = offsetIndex + 1
|
||||||
}
|
}
|
||||||
offsetResults.forEach { eventEntity ->
|
offsetResults.forEach { eventEntity ->
|
||||||
val timelineEvent = eventEntity.asDomain()
|
|
||||||
|
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||||
|
|
||||||
if (timelineEvent.isEncrypted()
|
if (timelineEvent.isEncrypted()
|
||||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||||
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,6 +502,12 @@ internal class DefaultTimeline(
|
||||||
return offsetResults.size
|
return offsetResults.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
|
||||||
|
timelineEventEntity = eventEntity,
|
||||||
|
buildReadReceipts = settings.buildReadReceipts,
|
||||||
|
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
@ -498,7 +525,6 @@ internal class DefaultTimeline(
|
||||||
.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
||||||
}
|
}
|
||||||
return offsetQuery
|
return offsetQuery
|
||||||
.filterAllowedTypes()
|
|
||||||
.limit(count)
|
.limit(count)
|
||||||
.findAll()
|
.findAll()
|
||||||
}
|
}
|
||||||
|
@ -545,7 +571,7 @@ internal class DefaultTimeline(
|
||||||
debouncer.debounce("post_snapshot", runnable, 50)
|
debouncer.debounce("post_snapshot", runnable, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension methods ***************************************************************************
|
// Extension methods ***************************************************************************
|
||||||
|
|
||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||||
|
@ -557,16 +583,20 @@ internal class DefaultTimeline(
|
||||||
} else {
|
} else {
|
||||||
sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
}
|
}
|
||||||
.filterAllowedTypes()
|
.filterEventsWithSettings()
|
||||||
.findFirst()
|
.findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RealmQuery<TimelineEventEntity>.filterAllowedTypes(): RealmQuery<TimelineEventEntity> {
|
private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> {
|
||||||
if (allowedTypes != null) {
|
if (settings.filterTypes) {
|
||||||
`in`(TimelineEventEntityFields.ROOT.TYPE, allowedTypes.toTypedArray())
|
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
|
||||||
|
}
|
||||||
|
if (settings.filterEdits) {
|
||||||
|
not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PaginationState(
|
private data class PaginationState(
|
||||||
|
|
|
@ -23,8 +23,10 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
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.session.room.timeline.TimelineService
|
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.internal.database.RealmLiveData
|
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
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
|
||||||
|
@ -36,10 +38,12 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val paginationTask: PaginationTask
|
private val paginationTask: PaginationTask,
|
||||||
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
|
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
|
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||||
return DefaultTimeline(roomId,
|
return DefaultTimeline(roomId,
|
||||||
eventId,
|
eventId,
|
||||||
monarchy.realmConfiguration,
|
monarchy.realmConfiguration,
|
||||||
|
@ -47,7 +51,10 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
|
||||||
contextOfEventTask,
|
contextOfEventTask,
|
||||||
paginationTask,
|
paginationTask,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
allowedTypes)
|
timelineEventMapper,
|
||||||
|
settings,
|
||||||
|
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||||
|
@ -55,7 +62,7 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
|
||||||
.fetchCopyMap({
|
.fetchCopyMap({
|
||||||
TimelineEventEntity.where(it, eventId = eventId).findFirst()
|
TimelineEventEntity.where(it, eventId = eventId).findFirst()
|
||||||
}, { entity, realm ->
|
}, { entity, realm ->
|
||||||
entity.asDomain()
|
timelineEventMapper.map(entity)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,8 +70,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
|
||||||
val liveData = RealmLiveData(monarchy.realmConfiguration) {
|
val liveData = RealmLiveData(monarchy.realmConfiguration) {
|
||||||
TimelineEventEntity.where(it, eventId = eventId)
|
TimelineEventEntity.where(it, eventId = eventId)
|
||||||
}
|
}
|
||||||
return Transformations.map(liveData) {
|
return Transformations.map(liveData) { events ->
|
||||||
it.firstOrNull()?.asDomain()
|
events.firstOrNull()?.let { timelineEventMapper.map(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
* 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.session.room.timeline
|
||||||
|
|
||||||
|
import android.util.SparseArray
|
||||||
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.query.FilterContent
|
||||||
|
import im.vector.matrix.android.internal.database.query.whereInRoom
|
||||||
|
import io.realm.OrderedRealmCollectionChangeListener
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.RealmResults
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering).
|
||||||
|
* When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event.
|
||||||
|
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
|
||||||
|
*/
|
||||||
|
internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||||
|
private val roomId: String,
|
||||||
|
private val settings: TimelineSettings) {
|
||||||
|
|
||||||
|
interface Delegate {
|
||||||
|
fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean
|
||||||
|
fun onReadReceiptsUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
|
||||||
|
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
||||||
|
|
||||||
|
private lateinit var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>
|
||||||
|
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
|
||||||
|
private lateinit var delegate: Delegate
|
||||||
|
|
||||||
|
private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { collection, changeSet ->
|
||||||
|
var hasChange = false
|
||||||
|
// Deletion here means we don't have any readReceipts for the given hidden events
|
||||||
|
changeSet.deletions.forEach {
|
||||||
|
val eventId = correctedReadReceiptsEventByIndex[it]
|
||||||
|
val timelineEvent = liveEvents.where()
|
||||||
|
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
|
||||||
|
.findFirst()
|
||||||
|
|
||||||
|
// We are rebuilding the corresponding event with only his own RR
|
||||||
|
val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)
|
||||||
|
hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange
|
||||||
|
}
|
||||||
|
correctedReadReceiptsEventByIndex.clear()
|
||||||
|
correctedReadReceiptsByEvent.clear()
|
||||||
|
hiddenReadReceipts.forEachIndexed { index, summary ->
|
||||||
|
val timelineEvent = summary?.timelineEvent?.firstOrNull()
|
||||||
|
val displayIndex = timelineEvent?.root?.displayIndex
|
||||||
|
if (displayIndex != null) {
|
||||||
|
// Then we are looking for the first displayable event after the hidden one
|
||||||
|
val firstDisplayedEvent = liveEvents.where()
|
||||||
|
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
|
||||||
|
.findFirst()
|
||||||
|
|
||||||
|
// If we find one, we should
|
||||||
|
if (firstDisplayedEvent != null) {
|
||||||
|
correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId)
|
||||||
|
correctedReadReceiptsByEvent
|
||||||
|
.getOrPut(firstDisplayedEvent.eventId, {
|
||||||
|
ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts))
|
||||||
|
})
|
||||||
|
.addAll(readReceiptsSummaryMapper.map(summary))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (correctedReadReceiptsByEvent.isNotEmpty()) {
|
||||||
|
correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) ->
|
||||||
|
val sortedReadReceipts = correctedReadReceipts.sortedByDescending {
|
||||||
|
it.originServerTs
|
||||||
|
}
|
||||||
|
hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasChange) {
|
||||||
|
delegate.onReadReceiptsUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the realm query subscription. Has to be called on an HandlerThread
|
||||||
|
*/
|
||||||
|
fun start(realm: Realm, liveEvents: RealmResults<TimelineEventEntity>, delegate: Delegate) {
|
||||||
|
this.liveEvents = liveEvents
|
||||||
|
this.delegate = delegate
|
||||||
|
// We are looking for read receipts set on hidden events.
|
||||||
|
// We only accept those with a timelineEvent (so coming from pagination/sync).
|
||||||
|
this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId)
|
||||||
|
.isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT)
|
||||||
|
.isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`)
|
||||||
|
.filterReceiptsWithSettings()
|
||||||
|
.findAllAsync()
|
||||||
|
.also { it.addChangeListener(hiddenReadReceiptsListener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose the realm query subscription. Has to be called on an HandlerThread
|
||||||
|
*/
|
||||||
|
fun dispose() {
|
||||||
|
this.hiddenReadReceipts.removeAllChangeListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current corrected [ReadReceipt] list for an event, or null
|
||||||
|
*/
|
||||||
|
fun correctedReadReceipts(eventId: String?): List<ReadReceipt>? {
|
||||||
|
return correctedReadReceiptsByEvent[eventId]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
|
||||||
|
*/
|
||||||
|
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
|
||||||
|
beginGroup()
|
||||||
|
if (settings.filterTypes) {
|
||||||
|
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
|
||||||
|
}
|
||||||
|
if (settings.filterTypes && settings.filterEdits) {
|
||||||
|
or()
|
||||||
|
}
|
||||||
|
if (settings.filterEdits) {
|
||||||
|
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
|
||||||
|
}
|
||||||
|
endGroup()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,10 @@
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.createUnmanaged
|
||||||
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -29,34 +33,70 @@ import javax.inject.Inject
|
||||||
// dict value ts value
|
// dict value ts value
|
||||||
typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>>
|
typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>>
|
||||||
|
|
||||||
|
private const val READ_KEY = "m.read"
|
||||||
|
private const val TIMESTAMP_KEY = "ts"
|
||||||
|
|
||||||
internal class ReadReceiptHandler @Inject constructor() {
|
internal class ReadReceiptHandler @Inject constructor() {
|
||||||
|
|
||||||
fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?) {
|
fun handle(realm: Realm, roomId: String, content: ReadReceiptContent?, isInitialSync: Boolean) {
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val readReceipts = mapContentToReadReceiptEntities(roomId, content)
|
handleReadReceiptContent(realm, roomId, content, isInitialSync)
|
||||||
realm.insertOrUpdate(readReceipts)
|
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Timber.e("Fail to handle read receipt for room $roomId")
|
Timber.e("Fail to handle read receipt for room $roomId")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapContentToReadReceiptEntities(roomId: String, content: ReadReceiptContent): List<ReadReceiptEntity> {
|
private fun handleReadReceiptContent(realm: Realm, roomId: String, content: ReadReceiptContent, isInitialSync: Boolean) {
|
||||||
return content
|
if (isInitialSync) {
|
||||||
.flatMap { (eventId, receiptDict) ->
|
initialSyncStrategy(realm, roomId, content)
|
||||||
receiptDict
|
} else {
|
||||||
.filterKeys { it == "m.read" }
|
incrementalSyncStrategy(realm, roomId, content)
|
||||||
.flatMap { (_, userIdsDict) ->
|
}
|
||||||
userIdsDict.map { (userId, paramsDict) ->
|
|
||||||
val ts = paramsDict.filterKeys { it == "ts" }
|
|
||||||
.values
|
|
||||||
.firstOrNull() ?: 0.0
|
|
||||||
val primaryKey = roomId + userId
|
|
||||||
ReadReceiptEntity(primaryKey, userId, eventId, roomId, ts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun initialSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
|
||||||
|
val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>()
|
||||||
|
for ((eventId, receiptDict) in content) {
|
||||||
|
val userIdsDict = receiptDict[READ_KEY] ?: continue
|
||||||
|
val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
|
||||||
|
|
||||||
|
for ((userId, paramsDict) in userIdsDict) {
|
||||||
|
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
|
||||||
|
val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts)
|
||||||
|
readReceiptsSummary.readReceipts.add(receiptEntity)
|
||||||
|
}
|
||||||
|
readReceiptSummaries.add(readReceiptsSummary)
|
||||||
|
}
|
||||||
|
realm.insertOrUpdate(readReceiptSummaries)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun incrementalSyncStrategy(realm: Realm, roomId: String, content: ReadReceiptContent) {
|
||||||
|
for ((eventId, receiptDict) in content) {
|
||||||
|
val userIdsDict = receiptDict[READ_KEY] ?: continue
|
||||||
|
val readReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
?: realm.createObject(ReadReceiptsSummaryEntity::class.java, eventId).apply {
|
||||||
|
this.roomId = roomId
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((userId, paramsDict) in userIdsDict) {
|
||||||
|
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
|
||||||
|
val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId)
|
||||||
|
// ensure new ts is superior to the previous one
|
||||||
|
if (ts > receiptEntity.originServerTs) {
|
||||||
|
ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also {
|
||||||
|
it.readReceipts.remove(receiptEntity)
|
||||||
|
}
|
||||||
|
receiptEntity.eventId = eventId
|
||||||
|
receiptEntity.originServerTs = ts
|
||||||
|
readReceiptsSummary.readReceipts.add(receiptEntity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -62,11 +62,11 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
|
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handle(roomsSyncResponse: RoomsSyncResponse, reporter: DefaultInitialSyncProgressService? = null) {
|
fun handle(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService? = null) {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
|
handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, reporter)
|
||||||
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
|
handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, reporter)
|
||||||
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
|
handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, reporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
//handle event for bing rule checks
|
//handle event for bing rule checks
|
||||||
|
@ -89,12 +89,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: DefaultInitialSyncProgressService?) {
|
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy, isInitialSync: Boolean, reporter: DefaultInitialSyncProgressService?) {
|
||||||
|
|
||||||
val rooms = when (handlingStrategy) {
|
val rooms = when (handlingStrategy) {
|
||||||
is HandlingStrategy.JOINED ->
|
is HandlingStrategy.JOINED ->
|
||||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) {
|
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_joined_rooms, 0.6f) {
|
||||||
handleJoinedRoom(realm, it.key, it.value)
|
handleJoinedRoom(realm, it.key, it.value, isInitialSync)
|
||||||
}
|
}
|
||||||
is HandlingStrategy.INVITED ->
|
is HandlingStrategy.INVITED ->
|
||||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) {
|
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) {
|
||||||
|
@ -112,12 +112,21 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
|
|
||||||
private fun handleJoinedRoom(realm: Realm,
|
private fun handleJoinedRoom(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
roomSync: RoomSync): RoomEntity {
|
roomSync: RoomSync,
|
||||||
|
isInitalSync: Boolean): RoomEntity {
|
||||||
|
|
||||||
Timber.v("Handle join sync for room $roomId")
|
Timber.v("Handle join sync for room $roomId")
|
||||||
|
|
||||||
|
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||||
|
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitalSync)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
|
||||||
|
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||||
|
}
|
||||||
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
if (roomEntity.membership == Membership.INVITE) {
|
if (roomEntity.membership == Membership.INVITE) {
|
||||||
roomEntity.chunks.deleteAllFromRealm()
|
roomEntity.chunks.deleteAllFromRealm()
|
||||||
|
@ -127,7 +136,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
// State event
|
// State event
|
||||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||||
?: Int.MIN_VALUE
|
?: Int.MIN_VALUE
|
||||||
val untimelinedStateIndex = minStateIndex + 1
|
val untimelinedStateIndex = minStateIndex + 1
|
||||||
roomSync.state.events.forEach { event ->
|
roomSync.state.events.forEach { event ->
|
||||||
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
||||||
|
@ -150,14 +159,6 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
}
|
}
|
||||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
||||||
|
|
||||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
|
||||||
handleEphemeral(realm, roomId, roomSync.ephemeral)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
|
|
||||||
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
|
||||||
}
|
|
||||||
return roomEntity
|
return roomEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +168,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
InvitedRoomSync): RoomEntity {
|
InvitedRoomSync): RoomEntity {
|
||||||
Timber.v("Handle invited sync for room $roomId")
|
Timber.v("Handle invited sync for room $roomId")
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
roomEntity.membership = Membership.INVITE
|
roomEntity.membership = Membership.INVITE
|
||||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||||
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
||||||
|
@ -181,7 +182,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
roomId: String,
|
roomId: String,
|
||||||
roomSync: RoomSync): RoomEntity {
|
roomSync: RoomSync): RoomEntity {
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
roomEntity.membership = Membership.LEAVE
|
roomEntity.membership = Membership.LEAVE
|
||||||
roomEntity.chunks.deleteAllFromRealm()
|
roomEntity.chunks.deleteAllFromRealm()
|
||||||
|
@ -233,17 +234,21 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
private fun handleEphemeral(realm: Realm,
|
private fun handleEphemeral(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
ephemeral: RoomSyncEphemeral) {
|
ephemeral: RoomSyncEphemeral,
|
||||||
ephemeral.events
|
isInitalSync: Boolean) {
|
||||||
.filter { it.getClearType() == EventType.RECEIPT }
|
for (event in ephemeral.events) {
|
||||||
.map { it.content.toModel<ReadReceiptContent>() }
|
if (event.type != EventType.RECEIPT) continue
|
||||||
.forEach { readReceiptHandler.handle(realm, roomId, it) }
|
val readReceiptContent = event.content as? ReadReceiptContent ?: continue
|
||||||
|
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitalSync)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||||
accountData.events
|
accountData.events
|
||||||
|
.asSequence()
|
||||||
.filter { it.getClearType() == EventType.TAG }
|
.filter { it.getClearType() == EventType.TAG }
|
||||||
.map { it.content.toModel<RoomTagContent>() }
|
.map { it.content.toModel<RoomTagContent>() }
|
||||||
.forEach { roomTagHandler.handle(realm, roomId, it) }
|
.forEach { roomTagHandler.handle(realm, roomId, it) }
|
||||||
|
|
|
@ -66,7 +66,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
|
||||||
|
|
||||||
reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) {
|
reportSubtask(reporter, R.string.initial_sync_start_importing_account_rooms, 100, 0.7f) {
|
||||||
if (syncResponse.rooms != null) {
|
if (syncResponse.rooms != null) {
|
||||||
roomSyncHandler.handle(syncResponse.rooms, reporter)
|
roomSyncHandler.handle(syncResponse.rooms, isInitialSync, reporter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
|
|
|
@ -14,15 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
package im.vector.riotx.core.date
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.format.DateUtils
|
||||||
import im.vector.riotx.core.resources.LocaleProvider
|
import im.vector.riotx.core.resources.LocaleProvider
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
import org.threeten.bp.format.DateTimeFormatter
|
import org.threeten.bp.format.DateTimeFormatter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
class TimelineDateFormatter @Inject constructor (private val localeProvider: LocaleProvider) {
|
class VectorDateFormatter @Inject constructor(private val context: Context,
|
||||||
|
private val localeProvider: LocaleProvider) {
|
||||||
|
|
||||||
private val messageHourFormatter by lazy {
|
private val messageHourFormatter by lazy {
|
||||||
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
|
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
|
||||||
|
@ -39,4 +42,16 @@ class TimelineDateFormatter @Inject constructor (private val localeProvider: Loc
|
||||||
return messageDayFormatter.format(localDateTime)
|
return messageDayFormatter.format(localDateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun formatRelativeDateTime(time: Long?): String {
|
||||||
|
if (time == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return DateUtils.getRelativeDateTimeString(context,
|
||||||
|
time,
|
||||||
|
DateUtils.DAY_IN_MILLIS,
|
||||||
|
2 * DateUtils.DAY_IN_MILLIS,
|
||||||
|
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||||
|
).toString()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -41,7 +41,12 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsers
|
||||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment
|
import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment
|
||||||
import im.vector.riotx.features.home.group.GroupListFragment
|
import im.vector.riotx.features.home.group.GroupListFragment
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
|
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
|
||||||
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
|
@ -168,6 +173,8 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(createDirectRoomActivity: CreateDirectRoomActivity)
|
fun inject(createDirectRoomActivity: CreateDirectRoomActivity)
|
||||||
|
|
||||||
|
fun inject(displayReadReceiptsBottomSheet: DisplayReadReceiptsBottomSheet)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(vectorComponent: VectorComponent,
|
fun create(vectorComponent: VectorComponent,
|
||||||
|
|
|
@ -29,7 +29,11 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
|
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
|
||||||
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
|
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
|
||||||
import im.vector.riotx.features.home.*
|
import im.vector.riotx.features.home.HomeActivityViewModel
|
||||||
|
import im.vector.riotx.features.home.HomeActivityViewModel_AssistedFactory
|
||||||
|
import im.vector.riotx.features.home.HomeDetailViewModel
|
||||||
|
import im.vector.riotx.features.home.HomeDetailViewModel_AssistedFactory
|
||||||
|
import im.vector.riotx.features.home.HomeNavigationViewModel
|
||||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel
|
import im.vector.riotx.features.home.createdirect.CreateDirectRoomNavigationViewModel
|
||||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel
|
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel
|
||||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory
|
import im.vector.riotx.features.home.createdirect.CreateDirectRoomViewModel_AssistedFactory
|
||||||
|
@ -39,7 +43,16 @@ import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory
|
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
|
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory
|
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel_AssistedFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel_AssistedFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionViewModel_AssistedFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryViewModel_AssistedFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.home.room.list.RoomListViewModel
|
import im.vector.riotx.features.home.room.list.RoomListViewModel
|
||||||
import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory
|
import im.vector.riotx.features.home.room.list.RoomListViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.reactions.EmojiChooserViewModel
|
import im.vector.riotx.features.reactions.EmojiChooserViewModel
|
||||||
|
|
|
@ -161,7 +161,6 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
unBinder?.unbind()
|
unBinder?.unbind()
|
||||||
unBinder = null
|
unBinder = null
|
||||||
|
|
||||||
|
|
|
@ -24,4 +24,9 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
|
||||||
fun shouldShowHiddenEvents(): Boolean {
|
fun shouldShowHiddenEvents(): Boolean {
|
||||||
return vectorPreferences.shouldShowHiddenEvents()
|
return vectorPreferences.shouldShowHiddenEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldShowReadReceipts(): Boolean {
|
||||||
|
return vectorPreferences.showReadReceipts()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.riotx.core.platform
|
package im.vector.riotx.core.ui.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
@ -32,10 +32,7 @@ import androidx.core.content.ContextCompat
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
|
||||||
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.toModel
|
|
||||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.error.ResourceLimitErrorFormatter
|
import im.vector.riotx.core.error.ResourceLimitErrorFormatter
|
||||||
import im.vector.riotx.features.themes.ThemeUtils
|
import im.vector.riotx.features.themes.ThemeUtils
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.core.ui.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
|
import kotlinx.android.synthetic.main.view_read_receipts.view.*
|
||||||
|
|
||||||
|
private const val MAX_RECEIPT_DISPLAYED = 5
|
||||||
|
|
||||||
|
class ReadReceiptsView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val receiptAvatars: List<ImageView> by lazy {
|
||||||
|
listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupView() {
|
||||||
|
inflate(context, R.layout.view_read_receipts, this)
|
||||||
|
ButterKnife.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer, clickListener: OnClickListener) {
|
||||||
|
setOnClickListener(clickListener)
|
||||||
|
if (readReceipts.isNotEmpty()) {
|
||||||
|
isVisible = true
|
||||||
|
for (index in 0 until MAX_RECEIPT_DISPLAYED) {
|
||||||
|
val receiptData = readReceipts.getOrNull(index)
|
||||||
|
if (receiptData == null) {
|
||||||
|
receiptAvatars[index].visibility = View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
receiptAvatars[index].visibility = View.VISIBLE
|
||||||
|
avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (readReceipts.size > MAX_RECEIPT_DISPLAYED) {
|
||||||
|
receiptMore.visibility = View.VISIBLE
|
||||||
|
receiptMore.text = context.getString(
|
||||||
|
R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
receiptMore.visibility = View.GONE
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -77,7 +77,7 @@ import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.extensions.setTextOrHide
|
import im.vector.riotx.core.extensions.setTextOrHide
|
||||||
import im.vector.riotx.core.files.addEntryToDownloadManager
|
import im.vector.riotx.core.files.addEntryToDownloadManager
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.platform.NotificationAreaView
|
import im.vector.riotx.core.ui.views.NotificationAreaView
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.utils.*
|
import im.vector.riotx.core.utils.*
|
||||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||||
|
@ -92,6 +92,7 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerActions
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
import im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
|
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
|
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
|
||||||
|
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||||
|
@ -325,17 +326,17 @@ class RoomDetailFragment :
|
||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse(messageContent.formattedBody
|
val document = parser.parse(messageContent.formattedBody
|
||||||
?: messageContent.body)
|
?: messageContent.body)
|
||||||
formattedBody = eventHtmlRenderer.render(document)
|
formattedBody = eventHtmlRenderer.render(document)
|
||||||
}
|
}
|
||||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||||
?: nonFormattedBody
|
?: nonFormattedBody
|
||||||
|
|
||||||
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
||||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
|
|
||||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||||
|
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
|
@ -364,9 +365,9 @@ class RoomDetailFragment :
|
||||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||||
REACTION_SELECT_REQUEST_CODE -> {
|
REACTION_SELECT_REQUEST_CODE -> {
|
||||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||||
?: return
|
?: return
|
||||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||||
?: return
|
?: return
|
||||||
//TODO check if already reacted with that?
|
//TODO check if already reacted with that?
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||||
}
|
}
|
||||||
|
@ -401,26 +402,26 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
||||||
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
||||||
R.drawable.ic_reply,
|
R.drawable.ic_reply,
|
||||||
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
(model as? AbsMessageItem)?.informationData?.let {
|
(model as? AbsMessageItem)?.informationData?.let {
|
||||||
val eventId = it.eventId
|
val eventId = it.eventId
|
||||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
return when (model) {
|
return when (model) {
|
||||||
is MessageFileItem,
|
is MessageFileItem,
|
||||||
is MessageImageVideoItem,
|
is MessageImageVideoItem,
|
||||||
is MessageTextItem -> {
|
is MessageTextItem -> {
|
||||||
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||||
touchHelper.attachToRecyclerView(recyclerView)
|
touchHelper.attachToRecyclerView(recyclerView)
|
||||||
}
|
}
|
||||||
|
@ -830,6 +831,11 @@ class RoomDetailFragment :
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) {
|
||||||
|
DisplayReadReceiptsBottomSheet.newInstance(readReceipts)
|
||||||
|
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
||||||
|
}
|
||||||
|
|
||||||
// AutocompleteUserPresenter.Callback
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
|
|
|
@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
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.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
|
@ -66,7 +67,7 @@ import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
||||||
userPreferencesProvider: UserPreferencesProvider,
|
private val userPreferencesProvider: UserPreferencesProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
||||||
|
@ -75,12 +76,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
private val roomId = initialState.roomId
|
private val roomId = initialState.roomId
|
||||||
private val eventId = initialState.eventId
|
private val eventId = initialState.eventId
|
||||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||||
private val allowedTypes = if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||||
TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES
|
TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts())
|
||||||
} else {
|
} else {
|
||||||
TimelineDisplayableEvents.DISPLAYABLE_TYPES
|
TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts())
|
||||||
}
|
}
|
||||||
private var timeline = room.createTimeline(eventId, allowedTypes)
|
|
||||||
|
private var timeline = room.createTimeline(eventId, timelineSettings)
|
||||||
|
|
||||||
// Slot to keep a pending action during permission request
|
// Slot to keep a pending action during permission request
|
||||||
var pendingAction: RoomDetailActions? = null
|
var pendingAction: RoomDetailActions? = null
|
||||||
|
@ -140,7 +142,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
|
|
||||||
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
|
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
|
||||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
val roomId = tombstoneContent.replacementRoom ?: ""
|
val roomId = tombstoneContent.replacementRoom ?: ""
|
||||||
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
||||||
|
@ -287,7 +289,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
|
|
||||||
//is original event a reply?
|
//is original event a reply?
|
||||||
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
//TODO check if same content?
|
//TODO check if same content?
|
||||||
room.getTimeLineEvent(inReplyTo)?.let {
|
room.getTimeLineEvent(inReplyTo)?.let {
|
||||||
|
@ -296,12 +298,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
} else {
|
} else {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val existingBody = messageContent?.body ?: ""
|
val existingBody = messageContent?.body ?: ""
|
||||||
if (existingBody != action.text) {
|
if (existingBody != action.text) {
|
||||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
||||||
?: "", messageContent?.type
|
?: "", messageContent?.type
|
||||||
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Same message content, do not send edition")
|
Timber.w("Same message content, do not send edition")
|
||||||
}
|
}
|
||||||
|
@ -316,7 +318,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is SendMode.QUOTE -> {
|
is SendMode.QUOTE -> {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val textMsg = messageContent?.body
|
val textMsg = messageContent?.body
|
||||||
|
|
||||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||||
|
@ -552,7 +554,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
} else {
|
} else {
|
||||||
// change timeline
|
// change timeline
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
timeline = room.createTimeline(targetEventId, allowedTypes)
|
timeline = room.createTimeline(targetEventId, timelineSettings)
|
||||||
timeline.start()
|
timeline.start()
|
||||||
|
|
||||||
withState {
|
withState {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.home.room.detail.readreceipts
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_display_read_receipt)
|
||||||
|
abstract class DisplayReadReceiptItem : EpoxyModelWithHolder<DisplayReadReceiptItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute var name: String? = null
|
||||||
|
@EpoxyAttribute var userId: String = ""
|
||||||
|
@EpoxyAttribute var avatarUrl: String? = null
|
||||||
|
@EpoxyAttribute var timestamp: CharSequence? = null
|
||||||
|
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
avatarRenderer.render(avatarUrl, userId, name, holder.avatarView)
|
||||||
|
holder.displayNameView.text = name ?: userId
|
||||||
|
timestamp?.let {
|
||||||
|
holder.timestampView.text = it
|
||||||
|
holder.timestampView.isVisible = true
|
||||||
|
} ?: run {
|
||||||
|
holder.timestampView.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val avatarView by bind<ImageView>(R.id.readReceiptAvatar)
|
||||||
|
val displayNameView by bind<TextView>(R.id.readReceiptName)
|
||||||
|
val timestampView by bind<TextView>(R.id.readReceiptDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.home.room.detail.readreceipts
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.args
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class DisplayReadReceiptArgs(
|
||||||
|
val readReceipts: List<ReadReceiptData>
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp
|
||||||
|
*/
|
||||||
|
class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
@Inject lateinit var epoxyController: DisplayReadReceiptsController
|
||||||
|
|
||||||
|
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||||
|
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||||
|
|
||||||
|
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
|
||||||
|
|
||||||
|
override fun injectWith(screenComponent: ScreenComponent) {
|
||||||
|
screenComponent.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
|
||||||
|
ButterKnife.bind(this, view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
epoxyRecyclerView.setController(epoxyController)
|
||||||
|
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
||||||
|
LinearLayout.VERTICAL)
|
||||||
|
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
|
bottomSheetTitle.text = getString(R.string.read_receipts_list)
|
||||||
|
epoxyController.setData(displayReadReceiptArgs.readReceipts)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() {
|
||||||
|
// we are not using state for this one as it's static
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {
|
||||||
|
val args = Bundle()
|
||||||
|
val parcelableArgs = DisplayReadReceiptArgs(
|
||||||
|
readReceipts
|
||||||
|
)
|
||||||
|
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
|
||||||
|
return DisplayReadReceiptsBottomSheet().apply { arguments = args }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.riotx.features.home.room.detail.readreceipts
|
||||||
|
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epoxy controller for read receipt event list
|
||||||
|
*/
|
||||||
|
class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||||
|
private val session: Session,
|
||||||
|
private val avatarRender: AvatarRenderer)
|
||||||
|
: TypedEpoxyController<List<ReadReceiptData>>() {
|
||||||
|
|
||||||
|
|
||||||
|
override fun buildModels(readReceipts: List<ReadReceiptData>) {
|
||||||
|
readReceipts.forEach {
|
||||||
|
val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp)
|
||||||
|
DisplayReadReceiptItem_()
|
||||||
|
.id(it.userId)
|
||||||
|
.userId(it.userId)
|
||||||
|
.avatarUrl(it.avatarUrl)
|
||||||
|
.name(it.displayName)
|
||||||
|
.avatarRenderer(avatarRender)
|
||||||
|
.timestamp(timestamp)
|
||||||
|
.addIf(session.myUserId != it.userId, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import com.airbnb.epoxy.EpoxyModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
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.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.core.epoxy.LoadingItem_
|
import im.vector.riotx.core.epoxy.LoadingItem_
|
||||||
import im.vector.riotx.core.extensions.localDateTime
|
import im.vector.riotx.core.extensions.localDateTime
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
|
@ -37,12 +38,13 @@ import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import im.vector.riotx.features.media.ImageContentRenderer
|
import im.vector.riotx.features.media.ImageContentRenderer
|
||||||
import im.vector.riotx.features.media.VideoContentRenderer
|
import im.vector.riotx.features.media.VideoContentRenderer
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineEventController @Inject constructor(private val dateFormatter: TimelineDateFormatter,
|
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||||
private val timelineItemFactory: TimelineItemFactory,
|
private val timelineItemFactory: TimelineItemFactory,
|
||||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
@ -51,7 +53,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
userPreferencesProvider: UserPreferencesProvider
|
userPreferencesProvider: UserPreferencesProvider
|
||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||||
|
|
||||||
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
|
interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
fun onRoomCreateLinkClicked(url: String)
|
fun onRoomCreateLinkClicked(url: String)
|
||||||
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
|
||||||
|
@ -77,6 +79,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
fun onMemberNameClicked(informationData: MessageInformationData)
|
fun onMemberNameClicked(informationData: MessageInformationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReadReceiptsCallback {
|
||||||
|
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
|
||||||
|
}
|
||||||
|
|
||||||
interface UrlClickCallback {
|
interface UrlClickCallback {
|
||||||
fun onUrlClicked(url: String): Boolean
|
fun onUrlClicked(url: String): Boolean
|
||||||
fun onUrlLongClicked(url: String): Boolean
|
fun onUrlLongClicked(url: String): Boolean
|
||||||
|
@ -158,7 +164,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
for (i in 0 until modelCache.size) {
|
for (i in 0 until modelCache.size) {
|
||||||
if (modelCache[i]?.eventId == eventIdToHighlight
|
if (modelCache[i]?.eventId == eventIdToHighlight
|
||||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||||
modelCache[i] = null
|
modelCache[i] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,8 +225,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||||
// We then are sure we always have items up to date.
|
// We then are sure we always have items up to date.
|
||||||
if (modelCache[position] == null
|
if (modelCache[position] == null
|
||||||
|| modelCache[position]?.mergedHeaderModel != null
|
|| modelCache[position]?.mergedHeaderModel != null
|
||||||
|| modelCache[position]?.formattedDayModel != null) {
|
|| modelCache[position]?.formattedDayModel != null) {
|
||||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,7 +299,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
||||||
// => handle case where paginating from mergeable events and we get more
|
// => handle case where paginating from mergeable events and we get more
|
||||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||||
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) ?: true
|
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
||||||
|
?: true
|
||||||
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
collapsedEventIds.addAll(mergedEventIds)
|
collapsedEventIds.addAll(mergedEventIds)
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||||
|
|
||||||
private val epoxyController by lazy {
|
private val epoxyController by lazy {
|
||||||
ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer)
|
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun injectWith(screenComponent: ScreenComponent) {
|
override fun injectWith(screenComponent: ScreenComponent) {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import im.vector.riotx.core.ui.list.genericFooterItem
|
||||||
import im.vector.riotx.core.ui.list.genericItem
|
import im.vector.riotx.core.ui.list.genericItem
|
||||||
import im.vector.riotx.core.ui.list.genericItemHeader
|
import im.vector.riotx.core.ui.list.genericItemHeader
|
||||||
import im.vector.riotx.core.ui.list.genericLoaderItem
|
import im.vector.riotx.core.ui.list.genericLoaderItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import name.fraser.neil.plaintext.diff_match_patch
|
import name.fraser.neil.plaintext.diff_match_patch
|
||||||
|
@ -44,7 +44,7 @@ import java.util.*
|
||||||
* Epoxy controller for reaction event list
|
* Epoxy controller for reaction event list
|
||||||
*/
|
*/
|
||||||
class ViewEditHistoryEpoxyController(private val context: Context,
|
class ViewEditHistoryEpoxyController(private val context: Context,
|
||||||
val timelineDateFormatter: TimelineDateFormatter,
|
val dateFormatter: VectorDateFormatter,
|
||||||
val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController<ViewEditHistoryViewState>() {
|
val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController<ViewEditHistoryViewState>() {
|
||||||
|
|
||||||
override fun buildModels(state: ViewEditHistoryViewState) {
|
override fun buildModels(state: ViewEditHistoryViewState) {
|
||||||
|
@ -84,7 +84,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
|
||||||
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
|
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
|
||||||
//need to display header with day
|
//need to display header with day
|
||||||
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
|
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
|
||||||
else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime())
|
else dateFormatter.formatMessageDay(timelineEvent.localDateTime())
|
||||||
genericItemHeader {
|
genericItemHeader {
|
||||||
id(evDate.hashCode())
|
id(evDate.hashCode())
|
||||||
text(dateString)
|
text(dateString)
|
||||||
|
@ -136,7 +136,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
|
||||||
}
|
}
|
||||||
genericItem {
|
genericItem {
|
||||||
id(timelineEvent.eventId)
|
id(timelineEvent.eventId)
|
||||||
title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime()))
|
title(dateFormatter.formatMessageHour(timelineEvent.localDateTime()))
|
||||||
description(spannedDiff ?: body)
|
description(spannedDiff ?: body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ data class ViewEditHistoryViewState(
|
||||||
class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
|
class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
|
||||||
initialState: ViewEditHistoryViewState,
|
initialState: ViewEditHistoryViewState,
|
||||||
val session: Session,
|
val session: Session,
|
||||||
val timelineDateFormatter: TimelineDateFormatter
|
val dateFormatter: VectorDateFormatter
|
||||||
) : VectorViewModel<ViewEditHistoryViewState>(initialState) {
|
) : VectorViewModel<ViewEditHistoryViewState>(initialState) {
|
||||||
|
|
||||||
private val roomId = initialState.roomId
|
private val roomId = initialState.roomId
|
||||||
|
|
|
@ -16,16 +16,20 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||||
|
|
||||||
import com.airbnb.mvrx.*
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
|
import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary
|
||||||
import im.vector.matrix.rx.RxRoom
|
import im.vector.matrix.rx.RxRoom
|
||||||
import im.vector.riotx.core.extensions.localDateTime
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.utils.isSingleEmoji
|
import im.vector.riotx.core.utils.isSingleEmoji
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
||||||
|
@ -54,13 +58,13 @@ data class ReactionInfo(
|
||||||
class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
||||||
initialState: DisplayReactionsViewState,
|
initialState: DisplayReactionsViewState,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val timelineDateFormatter: TimelineDateFormatter
|
private val dateFormatter: VectorDateFormatter
|
||||||
) : VectorViewModel<DisplayReactionsViewState>(initialState) {
|
) : VectorViewModel<DisplayReactionsViewState>(initialState) {
|
||||||
|
|
||||||
private val roomId = initialState.roomId
|
private val roomId = initialState.roomId
|
||||||
private val eventId = initialState.eventId
|
private val eventId = initialState.eventId
|
||||||
private val room = session.getRoom(roomId)
|
private val room = session.getRoom(roomId)
|
||||||
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
|
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
@ -100,14 +104,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
|
||||||
.fromIterable(summary.sourceEvents)
|
.fromIterable(summary.sourceEvents)
|
||||||
.map {
|
.map {
|
||||||
val event = room.getTimeLineEvent(it)
|
val event = room.getTimeLineEvent(it)
|
||||||
?: throw RuntimeException("Your eventId is not valid")
|
?: throw RuntimeException("Your eventId is not valid")
|
||||||
val localDate = event.root.localDateTime()
|
|
||||||
ReactionInfo(
|
ReactionInfo(
|
||||||
event.root.eventId!!,
|
event.root.eventId!!,
|
||||||
summary.key,
|
summary.key,
|
||||||
event.root.senderId ?: "",
|
event.root.senderId ?: "",
|
||||||
event.getDisambiguatedDisplayName(),
|
event.getDisambiguatedDisplayName(),
|
||||||
timelineDateFormatter.formatMessageHour(localDate)
|
dateFormatter.formatRelativeDateTime(event.root.originServerTs)
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.toList()
|
}.toList()
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.action
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
|
import android.text.format.DateUtils
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
import com.airbnb.mvrx.Fail
|
import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Incomplete
|
import com.airbnb.mvrx.Incomplete
|
||||||
|
|
|
@ -28,8 +28,15 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
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.session.room.timeline.getLastMessageContent
|
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
|
@ -47,7 +54,19 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
|
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
import im.vector.riotx.features.home.room.detail.timeline.item.BlankItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
|
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
import im.vector.riotx.features.media.ImageContentRenderer
|
import im.vector.riotx.features.media.ImageContentRenderer
|
||||||
|
@ -65,7 +84,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
private val imageContentRenderer: ImageContentRenderer,
|
private val imageContentRenderer: ImageContentRenderer,
|
||||||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
private val noticeItemFactory: NoticeItemFactory) {
|
||||||
|
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
|
@ -84,47 +103,26 @@ class MessageItemFactory @Inject constructor(
|
||||||
|
|
||||||
val messageContent: MessageContent =
|
val messageContent: MessageContent =
|
||||||
event.getLastMessageContent()
|
event.getLastMessageContent()
|
||||||
?: //Malformed content, we should echo something on screen
|
?: //Malformed content, we should echo something on screen
|
||||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||||
|
|
||||||
if (messageContent.relatesTo?.type == RelationType.REPLACE
|
if (messageContent.relatesTo?.type == RelationType.REPLACE
|
||||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||||
) {
|
) {
|
||||||
// ignore replace event, the targeted id is already edited
|
// This is an edit event, we should it when debugging as a notice event
|
||||||
if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
return noticeItemFactory.create(event, highlight, callback)
|
||||||
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
|
||||||
val informationData = MessageInformationData(
|
|
||||||
eventId = event.root.eventId ?: "?",
|
|
||||||
senderId = event.root.senderId ?: "",
|
|
||||||
sendState = event.root.sendState,
|
|
||||||
time = "",
|
|
||||||
avatarUrl = event.senderAvatar(),
|
|
||||||
memberName = "",
|
|
||||||
showInformation = false
|
|
||||||
)
|
|
||||||
return NoticeItem_()
|
|
||||||
.avatarRenderer(avatarRenderer)
|
|
||||||
.informationData(informationData)
|
|
||||||
.noticeText("{ \"type\": ${event.root.getClearType()} }")
|
|
||||||
.highlighted(highlight)
|
|
||||||
.baseCallback(callback)
|
|
||||||
} else {
|
|
||||||
return BlankItem_()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
return when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
|
||||||
informationData,
|
informationData,
|
||||||
highlight,
|
highlight,
|
||||||
callback)
|
callback)
|
||||||
is MessageTextContent -> buildTextMessageItem(event.root.sendState,
|
is MessageTextContent -> buildTextMessageItem(messageContent,
|
||||||
messageContent,
|
informationData,
|
||||||
informationData,
|
highlight,
|
||||||
highlight,
|
callback)
|
||||||
callback
|
|
||||||
)
|
|
||||||
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
|
||||||
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
|
||||||
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
|
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
|
||||||
|
@ -144,6 +142,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.filename(messageContent.body)
|
.filename(messageContent.body)
|
||||||
.iconRes(R.drawable.filetype_audio)
|
.iconRes(R.drawable.filetype_audio)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
|
@ -158,7 +157,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +173,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.filename(messageContent.body)
|
.filename(messageContent.body)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
.iconRes(R.drawable.filetype_attachment)
|
.iconRes(R.drawable.filetype_attachment)
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
|
@ -182,16 +182,12 @@ class MessageItemFactory @Inject constructor(
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
.clickListener(
|
.clickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { _ ->
|
DebouncedClickListener(View.OnClickListener { _ ->
|
||||||
callback?.onFileMessageClicked(informationData.eventId, messageContent)
|
callback?.onFileMessageClicked(informationData.eventId, messageContent)
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
|
||||||
?: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
|
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
|
||||||
|
@ -229,6 +225,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.mediaData(data)
|
.mediaData(data)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
.clickListener(
|
.clickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
@ -240,7 +237,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +250,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
val thumbnailData = ImageContentRenderer.Data(
|
val thumbnailData = ImageContentRenderer.Data(
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.videoInfo?.thumbnailFile?.url
|
url = messageContent.videoInfo?.thumbnailFile?.url
|
||||||
?: messageContent.videoInfo?.thumbnailUrl,
|
?: messageContent.videoInfo?.thumbnailUrl,
|
||||||
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
||||||
height = messageContent.videoInfo?.height,
|
height = messageContent.videoInfo?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
|
@ -280,6 +277,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.mediaData(thumbnailData)
|
.mediaData(thumbnailData)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
@ -288,12 +286,11 @@ class MessageItemFactory @Inject constructor(
|
||||||
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTextMessageItem(sendState: SendState,
|
private fun buildTextMessageItem(messageContent: MessageTextContent,
|
||||||
messageContent: MessageTextContent,
|
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
@ -320,6 +317,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.urlClickCallback(callback)
|
.urlClickCallback(callback)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
//click on the text
|
//click on the text
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
|
@ -328,7 +326,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,9 +356,9 @@ class MessageItemFactory @Inject constructor(
|
||||||
//nop
|
//nop
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editStart,
|
editStart,
|
||||||
editEnd,
|
editEnd,
|
||||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||||
return spannable
|
return spannable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,6 +384,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
.urlClickCallback(callback)
|
.urlClickCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
.memberClickListener(
|
.memberClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
@ -397,7 +396,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +424,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.reactionPillCallback(callback)
|
.reactionPillCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.urlClickCallback(callback)
|
.urlClickCallback(callback)
|
||||||
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
.emojiTypeFace(emojiCompatFontProvider.typeface)
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
|
@ -433,7 +433,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,13 +446,14 @@ class MessageItemFactory @Inject constructor(
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onEventCellClicked(informationData, null, view)
|
callback?.onEventCellClicked(informationData, null, view)
|
||||||
}))
|
}))
|
||||||
.longClickListener { view ->
|
.longClickListener { view ->
|
||||||
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
|
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,23 +25,18 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
|
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
|
||||||
private val avatarRenderer: AvatarRenderer) {
|
private val avatarRenderer: AvatarRenderer,
|
||||||
|
private val informationDataFactory: MessageInformationDataFactory) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||||
val formattedText = eventFormatter.format(event) ?: return null
|
val formattedText = eventFormatter.format(event) ?: return null
|
||||||
val informationData = MessageInformationData(
|
val informationData = informationDataFactory.create(event, null)
|
||||||
eventId = event.root.eventId ?: "?",
|
|
||||||
senderId = event.root.senderId ?: "",
|
|
||||||
sendState = event.root.sendState,
|
|
||||||
avatarUrl = event.senderAvatar(),
|
|
||||||
memberName = event.senderName(),
|
|
||||||
showInformation = false
|
|
||||||
)
|
|
||||||
|
|
||||||
return NoticeItem_()
|
return NoticeItem_()
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
@ -49,6 +44,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.baseCallback(callback)
|
.baseCallback(callback)
|
||||||
|
.readReceiptsCallback(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
|
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -33,8 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
private val encryptedItemFactory: EncryptedItemFactory,
|
private val encryptedItemFactory: EncryptedItemFactory,
|
||||||
private val noticeItemFactory: NoticeItemFactory,
|
private val noticeItemFactory: NoticeItemFactory,
|
||||||
private val defaultItemFactory: DefaultItemFactory,
|
private val defaultItemFactory: DefaultItemFactory,
|
||||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
private val roomCreateItemFactory: RoomCreateItemFactory) {
|
||||||
private val avatarRenderer: AvatarRenderer) {
|
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
|
@ -53,7 +53,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
EventType.STATE_HISTORY_VISIBILITY,
|
EventType.STATE_HISTORY_VISIBILITY,
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> noticeItemFactory.create(event, highlight, callback)
|
EventType.CALL_ANSWER,
|
||||||
|
EventType.REACTION,
|
||||||
|
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
|
||||||
// State room create
|
// State room create
|
||||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||||
// Crypto
|
// Crypto
|
||||||
|
@ -70,24 +72,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types (yet)
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STICKER -> defaultItemFactory.create(event, highlight)
|
EventType.STICKER -> defaultItemFactory.create(event, highlight)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
//These are just for debug to display hidden event, they should be filtered out in normal mode
|
Timber.v("Type ${event.root.getClearType()} not handled")
|
||||||
val informationData = MessageInformationData(
|
null
|
||||||
eventId = event.root.eventId ?: "?",
|
|
||||||
senderId = event.root.senderId ?: "",
|
|
||||||
sendState = event.root.sendState,
|
|
||||||
time = "",
|
|
||||||
avatarUrl = event.senderAvatar(),
|
|
||||||
memberName = "",
|
|
||||||
showInformation = false
|
|
||||||
)
|
|
||||||
NoticeItem_()
|
|
||||||
.avatarRenderer(avatarRenderer)
|
|
||||||
.informationData(informationData)
|
|
||||||
.noticeText("{ \"type\": ${event.root.getClearType()} }")
|
|
||||||
.highlighted(highlight)
|
|
||||||
.baseCallback(callback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.*
|
import im.vector.matrix.android.api.session.room.model.*
|
||||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
@ -42,6 +41,9 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
|
EventType.MESSAGE,
|
||||||
|
EventType.REACTION,
|
||||||
|
EventType.REDACTION -> formatDebug(timelineEvent.root)
|
||||||
else -> {
|
else -> {
|
||||||
Timber.v("Type $type not handled by this formatter")
|
Timber.v("Type $type not handled by this formatter")
|
||||||
null
|
null
|
||||||
|
@ -66,6 +68,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatDebug(event: Event): CharSequence? {
|
||||||
|
return "{ \"type\": ${event.getClearType()} }"
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
|
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
|
||||||
return if (!TextUtils.isEmpty(content.name)) {
|
return if (!TextUtils.isEmpty(content.name)) {
|
||||||
|
@ -90,7 +96,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||||
|
|
||||||
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val formattedVisibility = when (historyVisibility) {
|
val formattedVisibility = when (historyVisibility) {
|
||||||
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
||||||
|
@ -140,7 +146,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||||
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
|
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
|
||||||
else ->
|
else ->
|
||||||
stringProvider.getString(R.string.notice_display_name_changed_from,
|
stringProvider.getString(R.string.notice_display_name_changed_from,
|
||||||
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
|
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
|
||||||
}
|
}
|
||||||
displayText.append(displayNameText)
|
displayText.append(displayNameText)
|
||||||
}
|
}
|
||||||
|
@ -167,7 +173,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
|
||||||
when {
|
when {
|
||||||
eventContent.thirdPartyInvite != null ->
|
eventContent.thirdPartyInvite != null ->
|
||||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||||
TextUtils.equals(event.stateKey, selfUserId) ->
|
TextUtils.equals(event.stateKey, selfUserId) ->
|
||||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||||
event.stateKey.isNullOrEmpty() ->
|
event.stateKey.isNullOrEmpty() ->
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
@ -39,7 +40,6 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
|
||||||
import im.vector.riotx.features.reactions.widget.ReactionButton
|
import im.vector.riotx.features.reactions.widget.ReactionButton
|
||||||
import im.vector.riotx.features.ui.getMessageTextColor
|
import im.vector.riotx.features.ui.getMessageTextColor
|
||||||
|
|
||||||
|
|
||||||
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
|
@ -69,6 +69,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var avatarCallback: TimelineEventController.AvatarCallback? = null
|
var avatarCallback: TimelineEventController.AvatarCallback? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
|
||||||
|
|
||||||
private val _avatarClickListener = DebouncedClickListener(View.OnClickListener {
|
private val _avatarClickListener = DebouncedClickListener(View.OnClickListener {
|
||||||
avatarCallback?.onAvatarClicked(informationData)
|
avatarCallback?.onAvatarClicked(informationData)
|
||||||
})
|
})
|
||||||
|
@ -76,6 +79,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
avatarCallback?.onMemberNameClicked(informationData)
|
avatarCallback?.onMemberNameClicked(informationData)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
|
||||||
|
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
|
||||||
|
})
|
||||||
|
|
||||||
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
|
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
|
||||||
override fun onReacted(reactionButton: ReactionButton) {
|
override fun onReacted(reactionButton: ReactionButton) {
|
||||||
|
@ -123,6 +129,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
holder.memberNameView.setOnLongClickListener(null)
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
|
||||||
|
|
||||||
if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
|
if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
|
||||||
holder.reactionWrapper?.isVisible = false
|
holder.reactionWrapper?.isVisible = false
|
||||||
} else {
|
} else {
|
||||||
|
@ -173,7 +181,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||||
var reactionWrapper: ViewGroup? = null
|
var reactionWrapper: ViewGroup? = null
|
||||||
var reactionFlowHelper: Flow? = null
|
var reactionFlowHelper: Flow? = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,8 @@ data class MessageInformationData(
|
||||||
/*List of reactions (emoji,count,isSelected)*/
|
/*List of reactions (emoji,count,isSelected)*/
|
||||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||||
val hasBeenEdited: Boolean = false,
|
val hasBeenEdited: Boolean = false,
|
||||||
val hasPendingEdits: Boolean = false
|
val hasPendingEdits: Boolean = false,
|
||||||
|
val readReceipts: List<ReadReceiptData> = emptyList()
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,3 +44,11 @@ data class ReactionInfoData(
|
||||||
val addedByMe: Boolean,
|
val addedByMe: Boolean,
|
||||||
val synced: Boolean
|
val synced: Boolean
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ReadReceiptData(
|
||||||
|
val userId: String,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val displayName: String?,
|
||||||
|
val timestamp: Long
|
||||||
|
) : Parcelable
|
|
@ -22,6 +22,8 @@ import android.widget.TextView
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
|
@ -44,6 +46,13 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||||
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
|
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
|
||||||
|
|
||||||
|
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
|
||||||
|
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
|
||||||
|
})
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.noticeTextView.text = noticeText
|
holder.noticeTextView.text = noticeText
|
||||||
|
@ -55,6 +64,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||||
holder.avatarImageView
|
holder.avatarImageView
|
||||||
)
|
)
|
||||||
holder.view.setOnLongClickListener(longClickListener)
|
holder.view.setOnLongClickListener(longClickListener)
|
||||||
|
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
@ -62,6 +72,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||||
class Holder : BaseHolder(STUB_ID) {
|
class Holder : BaseHolder(STUB_ID) {
|
||||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||||
|
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.util
|
package im.vector.riotx.features.home.room.detail.timeline.util
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
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.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
|
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
|
||||||
|
@ -23,16 +24,18 @@ import im.vector.riotx.core.extensions.localDateTime
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
import im.vector.riotx.core.utils.isSingleEmoji
|
import im.vector.riotx.core.utils.isSingleEmoji
|
||||||
import im.vector.riotx.features.home.getColorFromUserId
|
import im.vector.riotx.features.home.getColorFromUserId
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
|
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
|
||||||
*/
|
*/
|
||||||
class MessageInformationDataFactory @Inject constructor(private val timelineDateFormatter: TimelineDateFormatter,
|
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||||
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider) {
|
private val colorProvider: ColorProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
||||||
|
@ -43,21 +46,20 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
|
||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||||
?: false
|
?: false
|
||||||
|
|
||||||
val showInformation =
|
val showInformation =
|
||||||
addDaySeparator
|
addDaySeparator
|
||||||
|| event.senderAvatar != nextEvent?.senderAvatar
|
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||||
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName()
|
||||||
|| (nextEvent?.root?.getClearType() != EventType.MESSAGE && nextEvent?.root?.getClearType() != EventType.ENCRYPTED)
|
|| (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED)
|
||||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||||
|
|
||||||
val time = timelineDateFormatter.formatMessageHour(date)
|
val time = dateFormatter.formatMessageHour(date)
|
||||||
val avatarUrl = event.senderAvatar
|
val avatarUrl = event.senderAvatar
|
||||||
val memberName = event.getDisambiguatedDisplayName()
|
val memberName = event.getDisambiguatedDisplayName()
|
||||||
val formattedMemberName = span(memberName) {
|
val formattedMemberName = span(memberName) {
|
||||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId
|
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
|
||||||
?: ""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
|
@ -74,7 +76,16 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
|
||||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||||
},
|
},
|
||||||
hasBeenEdited = event.hasBeenEdited(),
|
hasBeenEdited = event.hasBeenEdited(),
|
||||||
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false
|
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
|
||||||
|
readReceipts = event.readReceipts
|
||||||
|
.asSequence()
|
||||||
|
.filter {
|
||||||
|
it.user.userId != session.myUserId
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -29,13 +29,13 @@ import im.vector.riotx.core.resources.DateProvider
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
|
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
|
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter,
|
class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter,
|
||||||
private val timelineDateFormatter: TimelineDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val avatarRenderer: AvatarRenderer) {
|
private val avatarRenderer: AvatarRenderer) {
|
||||||
|
@ -94,7 +94,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
|
||||||
val currentDate = DateProvider.currentLocalDateTime()
|
val currentDate = DateProvider.currentLocalDateTime()
|
||||||
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
|
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
|
||||||
latestFormattedEvent = if (latestEvent.root.isEncrypted()
|
latestFormattedEvent = if (latestEvent.root.isEncrypted()
|
||||||
&& latestEvent.root.mxDecryptionResult == null) {
|
&& latestEvent.root.mxDecryptionResult == null) {
|
||||||
stringProvider.getString(R.string.encrypted_message)
|
stringProvider.getString(R.string.encrypted_message)
|
||||||
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
|
} else if (latestEvent.root.getClearType() == EventType.MESSAGE) {
|
||||||
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
|
val senderName = latestEvent.senderName() ?: latestEvent.root.senderId
|
||||||
|
@ -117,10 +117,9 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
latestEventTime = if (isSameDay) {
|
latestEventTime = if (isSameDay) {
|
||||||
timelineDateFormatter.formatMessageHour(date)
|
dateFormatter.formatMessageHour(date)
|
||||||
} else {
|
} else {
|
||||||
//TODO: change this
|
dateFormatter.formatMessageDay(date)
|
||||||
timelineDateFormatter.formatMessageDay(date)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return RoomSummaryItem_()
|
return RoomSummaryItem_()
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<im.vector.riotx.core.platform.NotificationAreaView
|
<im.vector.riotx.core.ui.views.NotificationAreaView
|
||||||
android:id="@+id/notificationAreaView"
|
android:id="@+id/notificationAreaView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
42
vector/src/main/res/layout/item_display_read_receipt.xml
Normal file
42
vector/src/main/res/layout/item_display_read_receipt.xml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/readReceiptAvatar"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/readReceiptName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="@sample/matrix.json/data/displayName" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/readReceiptDate"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:lines="1"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="10:44" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -2,11 +2,10 @@
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="44dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingStart="8dp"
|
android:paddingStart="8dp"
|
||||||
android:paddingLeft="8dp"
|
|
||||||
android:paddingEnd="8dp">
|
android:paddingEnd="8dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -26,7 +25,8 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
android:layout_marginLeft="4dp"
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:lines="1"
|
android:lines="1"
|
||||||
|
|
|
@ -114,7 +114,7 @@
|
||||||
android:inflatedId="@+id/messageBottomInfo"
|
android:inflatedId="@+id/messageBottomInfo"
|
||||||
android:layout="@layout/item_timeline_event_bottom_reactions_stub"
|
android:layout="@layout/item_timeline_event_bottom_reactions_stub"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toTopOf="@+id/readReceiptsView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
@ -123,4 +123,14 @@
|
||||||
</ViewStub>
|
</ViewStub>
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
|
android:id="@+id/readReceiptsView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -52,5 +52,14 @@
|
||||||
android:layout="@layout/item_timeline_event_merged_header_stub"
|
android:layout="@layout/item_timeline_event_merged_header_stub"
|
||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
|
android:id="@+id/readReceiptsView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
58
vector/src/main/res/layout/view_read_receipts.xml
Normal file
58
vector/src/main/res/layout/view_read_receipts.xml
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:parentTag="android.widget.LinearLayout">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/receiptMore"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:text="999+" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/receiptAvatar5"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/receiptAvatar4"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/receiptAvatar3"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/receiptAvatar2"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/receiptAvatar1"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
</merge>
|
|
@ -297,6 +297,7 @@
|
||||||
|
|
||||||
<style name="TimelineContentStubNoInfoLayoutParams" parent="TimelineContentStubBaseParams">
|
<style name="TimelineContentStubNoInfoLayoutParams" parent="TimelineContentStubBaseParams">
|
||||||
<item name="layout_constraintTop_toTopOf">parent</item>
|
<item name="layout_constraintTop_toTopOf">parent</item>
|
||||||
|
<item name="layout_constraintBottom_toTopOf">@id/readReceiptsView</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue