Read receipts: handle read receipts set on filtered events + let BottomSheet takes a snapshot instead of being live.

This commit is contained in:
ganfra 2019-08-12 17:59:07 +02:00
parent 70639f180c
commit 21deb2551d
25 changed files with 277 additions and 285 deletions

View file

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

View file

@ -0,0 +1,39 @@
/*
* 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()
)

View file

@ -140,7 +140,7 @@ internal fun ChunkEntity.add(roomId: String,
val senderId = event.senderId ?: "" val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: ReadReceiptsSummaryEntity(eventId) ?: ReadReceiptsSummaryEntity(eventId, roomId)
// Update RR for the sender of a new message with a dummy one // Update RR for the sender of a new message with a dummy one

View file

@ -28,7 +28,10 @@ import javax.inject.Inject
internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) {
fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity): List<ReadReceipt> { fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
if (readReceiptsSummaryEntity == null) {
return emptyList()
}
return Realm.getInstance(realmConfiguration).use { realm -> return Realm.getInstance(realmConfiguration).use { realm ->
val readReceipts = readReceiptsSummaryEntity.readReceipts val readReceipts = readReceiptsSummaryEntity.readReceipts
readReceipts readReceipts

View file

@ -17,15 +17,18 @@
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 import javax.inject.Inject
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper){ internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
fun map(timelineEventEntity: TimelineEventEntity): TimelineEvent {
fun map(timelineEventEntity: TimelineEventEntity, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
val readReceipts = correctedReadReceipts ?: timelineEventEntity.readReceipts?.let {
readReceiptsSummaryMapper.map(it)
}
return TimelineEvent( return TimelineEvent(
root = timelineEventEntity.root?.asDomain() root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId), ?: Event("", timelineEventEntity.eventId),
@ -35,9 +38,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderName = timelineEventEntity.senderName, senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar, senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = timelineEventEntity.readReceipts?.let { readReceipts = readReceipts ?: emptyList()
readReceiptsSummaryMapper.map(it)
} ?: emptyList()
) )
} }

View file

@ -18,14 +18,20 @@ package im.vector.matrix.android.internal.database.model
import io.realm.RealmList import io.realm.RealmList
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 ReadReceiptsSummaryEntity( internal open class ReadReceiptsSummaryEntity(
@PrimaryKey @PrimaryKey
var eventId: String = "", var eventId: String = "",
var roomId: String = "",
var readReceipts: RealmList<ReadReceiptEntity> = RealmList() var readReceipts: RealmList<ReadReceiptEntity> = RealmList()
) : RealmObject() { ) : RealmObject() {
@LinkingObjects("readReceipts")
val timelineEvent: RealmResults<TimelineEventEntity>? = null
companion object companion object
} }

View file

@ -26,3 +26,11 @@ internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: St
return realm.where<ReadReceiptsSummaryEntity>() return realm.where<ReadReceiptsSummaryEntity>()
.equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) .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
}

View file

@ -65,7 +65,7 @@ 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, timelineEventMapper) 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)

View file

@ -16,25 +16,47 @@
package im.vector.matrix.android.internal.session.room.timeline package im.vector.matrix.android.internal.session.room.timeline
import android.util.SparseArray
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.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper 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.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntityFields
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.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
@ -43,10 +65,11 @@ 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
private const val EDIT_FILTER_LIKE = "{*\"m.relates_to\"*\"rel_type\":*\"m.replace\"*}"
internal class DefaultTimeline( internal class DefaultTimeline(
private val roomId: String, private val roomId: String,
private val initialEventId: String? = null, private val initialEventId: String? = null,
@ -56,7 +79,8 @@ internal class DefaultTimeline(
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val allowedTypes: List<String>? private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val settings: TimelineSettings
) : Timeline { ) : Timeline {
private companion object { private companion object {
@ -79,6 +103,11 @@ 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 hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>? = null
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private var roomEntity: RoomEntity? = null private var roomEntity: RoomEntity? = null
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
@ -92,7 +121,6 @@ internal class DefaultTimeline(
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)
@ -132,9 +160,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] = timelineEventMapper.map(eventEntity) builtEvents[builtIndex] = timelineEventMapper.map(eventEntity, correctedReadReceiptsByEvent[te.root.eventId])
hasChanged = true hasChanged = true
} }
} }
@ -164,32 +192,54 @@ internal class DefaultTimeline(
postSnapshot() postSnapshot()
} }
// private val newSessionListener = object : NewSessionListener { private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { collection, changeSet ->
// override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) { var hasChange = false
// if (roomId == this@DefaultTimeline.roomId) { changeSet.deletions.forEach {
// Timber.v("New session id detected for this room") val eventId = correctedReadReceiptsEventByIndex[it]
// BACKGROUND_HANDLER.post { val timelineEvent = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst()
// val realm = backgroundRealm.get() builtEventsIdMap[eventId]?.let { builtIndex ->
// var hasChange = false builtEvents[builtIndex]?.let { te ->
// builtEvents.forEachIndexed { index, timelineEvent -> builtEvents[builtIndex] = te.copy(readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts))
// if (timelineEvent.isEncrypted()) { hasChange = true
// 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 correctedReadReceiptsEventByIndex.clear()
// EventEntity.where(realm, eventId = timelineEvent.root.eventId!!).findFirst()?.let { correctedReadReceiptsByEvent.clear()
// //builtEvents[index] = timelineEventFactory.create(it, realm) val loadedReadReceipts = collection.where().greaterThan("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.DISPLAY_INDEX}", prevDisplayIndex).findAll()
// hasChange = true loadedReadReceipts.forEachIndexed { index, summary ->
// } val timelineEvent = summary?.timelineEvent?.firstOrNull()
// } val displayIndex = timelineEvent?.root?.displayIndex
// } if (displayIndex != null) {
// } val firstDisplayedEvent = liveEvents.where()
// if (hasChange) postSnapshot() .sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
// } .lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
// } .findFirst()
// }
// if (firstDisplayedEvent != null) {
// } correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId)
correctedReadReceiptsByEvent.getOrPut(firstDisplayedEvent.eventId, {
readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts).toMutableList()
}).addAll(
readReceiptsSummaryMapper.map(summary)
)
}
}
}
if (correctedReadReceiptsByEvent.isNotEmpty()) {
correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) ->
builtEventsIdMap[eventId]?.let { builtIndex ->
builtEvents[builtIndex]?.let { te ->
builtEvents[builtIndex] = te.copy(readReceipts = correctedReadReceipts)
hasChange = true
}
}
}
}
if (hasChange) {
postSnapshot()
}
}
// Public methods ****************************************************************************** // Public methods ******************************************************************************
@ -236,15 +286,23 @@ 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) }
hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId)
.isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT)
.isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`)
.filterReceiptsWithSettings()
.findAllAsync()
.also { it.addChangeListener(hiddenReadReceiptsListener) }
isReady.set(true)
} }
} }
} }
@ -257,6 +315,7 @@ internal class DefaultTimeline(
cancelableBag.cancel() cancelableBag.cancel()
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
eventRelations.removeAllChangeListeners() eventRelations.removeAllChangeListeners()
hiddenReadReceipts?.removeAllChangeListeners()
liveEvents.removeAllChangeListeners() liveEvents.removeAllChangeListeners()
backgroundRealm.getAndSet(null).also { backgroundRealm.getAndSet(null).also {
it.close() it.close()
@ -274,7 +333,7 @@ internal class DefaultTimeline(
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
@ -331,7 +390,9 @@ 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(timelineEventMapper.map(it)) sendingEvents.add(timelineEventMapper.map(it))
} }
@ -380,7 +441,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 {
@ -397,9 +458,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
@ -465,10 +526,11 @@ internal class DefaultTimeline(
nextDisplayIndex = offsetIndex + 1 nextDisplayIndex = offsetIndex + 1
} }
offsetResults.forEach { eventEntity -> offsetResults.forEach { eventEntity ->
val timelineEvent = timelineEventMapper.map(eventEntity) val timelineEvent = timelineEventMapper.map(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) }
} }
@ -500,7 +562,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()
} }
@ -559,14 +620,35 @@ 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, EDIT_FILTER_LIKE)
}
return this
}
/**
* We are looking for receipts related to filtered events. So, it's the opposite of [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}", EDIT_FILTER_LIKE)
}
endGroup()
return this return this
} }
} }

View file

@ -23,7 +23,9 @@ 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.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper 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.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -38,10 +40,11 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
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 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,
@ -50,7 +53,8 @@ internal class DefaultTimelineService @Inject constructor(private val roomId: St
paginationTask, paginationTask,
cryptoService, cryptoService,
timelineEventMapper, timelineEventMapper,
allowedTypes readReceiptsSummaryMapper,
settings
) )
} }

View file

@ -62,7 +62,7 @@ internal class ReadReceiptHandler @Inject constructor() {
val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>() val readReceiptSummaries = ArrayList<ReadReceiptsSummaryEntity>()
for ((eventId, receiptDict) in content) { for ((eventId, receiptDict) in content) {
val userIdsDict = receiptDict[READ_KEY] ?: continue val userIdsDict = receiptDict[READ_KEY] ?: continue
val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId) val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
for ((userId, paramsDict) in userIdsDict) { for ((userId, paramsDict) in userIdsDict) {
val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0 val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0

View file

@ -43,8 +43,6 @@ 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.readreceipts.DisplayReadReceiptsViewModel
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsViewModel 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.MessageActionsViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel
@ -197,8 +195,4 @@ interface ViewModelModule {
@Binds @Binds
fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory fun bindPushGatewaysViewModelFactory(factory: PushGatewaysViewModel_AssistedFactory): PushGatewaysViewModel.Factory
@Binds
fun bindDisplayReadReceiptsViewModel(factory: DisplayReadReceiptsViewModel_AssistedFactory): DisplayReadReceiptsViewModel.Factory
} }

View file

@ -817,8 +817,8 @@ class RoomDetailFragment :
}) })
} }
override fun onReadReceiptsClicked(informationData: MessageInformationData) { override fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) {
DisplayReadReceiptsBottomSheet.newInstance(roomDetailArgs.roomId, informationData) DisplayReadReceiptsBottomSheet.newInstance(readReceipts)
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
} }

View file

@ -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
@ -73,12 +74,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)
} else { } else {
TimelineDisplayableEvents.DISPLAYABLE_TYPES TimelineSettings(30, true, true, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
} }
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
@ -137,7 +139,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
@ -283,7 +285,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 {
@ -292,12 +294,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")
} }
@ -312,7 +314,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)
@ -550,7 +552,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 {

View file

@ -16,8 +16,8 @@
package im.vector.riotx.features.home.room.detail.readreceipts package im.vector.riotx.features.home.room.detail.readreceipts
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -27,31 +27,31 @@ import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.args
import com.airbnb.mvrx.withState
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.* import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import javax.inject.Inject 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 * Bottom sheet displaying list of read receipts for a given event ordered by descending timestamp
*/ */
class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val viewModel: DisplayReadReceiptsViewModel by fragmentViewModel()
@Inject lateinit var displayReadReceiptsViewModelFactory: DisplayReadReceiptsViewModel.Factory
@Inject lateinit var epoxyController: DisplayReadReceiptsController @Inject lateinit var epoxyController: DisplayReadReceiptsController
@BindView(R.id.bottom_sheet_display_reactions_list) @BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView lateinit var epoxyRecyclerView: EpoxyRecyclerView
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this) screenComponent.inject(this)
@ -70,20 +70,18 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
LinearLayout.VERTICAL) LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration) epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = getString(R.string.read_receipts_list) bottomSheetTitle.text = getString(R.string.read_receipts_list)
epoxyController.setData(displayReadReceiptArgs.readReceipts)
} }
override fun invalidate() {
override fun invalidate() = withState(viewModel) { // we are not using state for this one as it's static
epoxyController.setData(it)
} }
companion object { companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): DisplayReadReceiptsBottomSheet { fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {
val args = Bundle() val args = Bundle()
val parcelableArgs = TimelineEventFragmentArgs( val parcelableArgs = DisplayReadReceiptArgs(
informationData.eventId, readReceipts
roomId,
informationData
) )
args.putParcelable(MvRx.KEY_ARG, parcelableArgs) args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return DisplayReadReceiptsBottomSheet().apply { arguments = args } return DisplayReadReceiptsBottomSheet().apply { arguments = args }

View file

@ -17,55 +17,32 @@
package im.vector.riotx.features.home.room.detail.readreceipts package im.vector.riotx.features.home.room.detail.readreceipts
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericLoaderItem
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import javax.inject.Inject import javax.inject.Inject
/** /**
* Epoxy controller for read receipt event list * Epoxy controller for read receipt event list
*/ */
class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter, class DisplayReadReceiptsController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val session: Session, private val session: Session,
private val avatarRender: AvatarRenderer) private val avatarRender: AvatarRenderer)
: TypedEpoxyController<DisplayReadReceiptsViewState>() { : TypedEpoxyController<List<ReadReceiptData>>() {
override fun buildModels(state: DisplayReadReceiptsViewState) { override fun buildModels(readReceipts: List<ReadReceiptData>) {
when (state.readReceipts) { readReceipts.forEach {
is Incomplete -> { val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp)
genericLoaderItem { DisplayReadReceiptItem_()
id("loading") .id(it.userId)
} .userId(it.userId)
} .avatarUrl(it.avatarUrl)
is Fail -> { .name(it.displayName)
genericFooterItem { .avatarRenderer(avatarRender)
id("failure") .timestamp(timestamp)
text(stringProvider.getString(R.string.unknown_error)) .addIf(session.myUserId != it.userId, this)
}
}
is Success -> {
state.readReceipts()?.forEach {
val timestamp = dateFormatter.formatRelativeDateTime(it.originServerTs)
DisplayReadReceiptItem_()
.id(it.user.userId)
.userId(it.user.userId)
.avatarUrl(it.user.avatarUrl)
.name(it.user.displayName)
.avatarRenderer(avatarRender)
.timestamp(timestamp)
.addIf(session.myUserId != it.user.userId, this)
}
}
} }
} }
} }

View file

@ -1,63 +0,0 @@
/*
* 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.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.RxRoom
import im.vector.riotx.core.platform.VectorViewModel
/**
* Used to display the list of read receipts to a given event
*/
class DisplayReadReceiptsViewModel @AssistedInject constructor(@Assisted initialState: DisplayReadReceiptsViewState,
private val session: Session
) : VectorViewModel<DisplayReadReceiptsViewState>(initialState) {
private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val room = session.getRoom(roomId)
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
@AssistedInject.Factory
interface Factory {
fun create(initialState: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel
}
companion object : MvRxViewModelFactory<DisplayReadReceiptsViewModel, DisplayReadReceiptsViewState> {
override fun create(viewModelContext: ViewModelContext, state: DisplayReadReceiptsViewState): DisplayReadReceiptsViewModel? {
val fragment: DisplayReadReceiptsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.displayReadReceiptsViewModelFactory.create(state)
}
}
init {
observeEventAnnotationSummaries()
}
private fun observeEventAnnotationSummaries() {
RxRoom(room)
.liveEventReadReceipts(eventId)
.execute {
copy(readReceipts = it)
}
}
}

View file

@ -1,33 +0,0 @@
/*
* 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.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
data class DisplayReadReceiptsViewState(
val eventId: String,
val roomId: String,
val readReceipts: Async<List<ReadReceipt>> = Uninitialized
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId)
}

View file

@ -38,6 +38,7 @@ 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
@ -79,7 +80,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
interface ReadReceiptsCallback { interface ReadReceiptsCallback {
fun onReadReceiptsClicked(informationData: MessageInformationData) fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
} }
interface UrlClickCallback { interface UrlClickCallback {

View file

@ -84,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,
@ -109,27 +109,8 @@ class MessageItemFactory @Inject constructor(
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>()

View file

@ -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) {

View file

@ -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() ->

View file

@ -80,7 +80,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
}) })
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
readReceiptsCallback?.onReadReceiptsClicked(informationData) readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
}) })
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {

View file

@ -50,7 +50,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
readReceiptsCallback?.onReadReceiptsClicked(informationData) readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
}) })
override fun bind(holder: Holder) { override fun bind(holder: Holder) {

View file

@ -52,15 +52,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
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 = dateFormatter.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(