Timeline: refact [WIP]

This commit is contained in:
ganfra 2019-09-25 19:14:12 +02:00
parent 63b43de4b8
commit 4a80df082c
25 changed files with 266 additions and 246 deletions

View File

@ -38,10 +38,12 @@ internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, r
} }
internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, eventId: String): EventAnnotationsSummaryEntity { internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId: String, eventId: String): EventAnnotationsSummaryEntity {
val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId) val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId).apply {
this.roomId = roomId
}
//Denormalization //Denormalization
TimelineEventEntity.where(realm, eventId = eventId).findFirst()?.let { TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
it.annotations = obj it.annotations = obj
} }
return obj return obj

View File

@ -27,10 +27,7 @@ internal fun ReadReceiptsSummaryEntity.Companion.where(realm: Realm, eventId: St
.equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId) .equalTo(ReadReceiptsSummaryEntityFields.EVENT_ID, eventId)
} }
internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery<ReadReceiptsSummaryEntity> { internal fun ReadReceiptsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String): RealmQuery<ReadReceiptsSummaryEntity> {
val query = realm.where<ReadReceiptsSummaryEntity>() return realm.where<ReadReceiptsSummaryEntity>()
if (roomId != null) { .equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId)
query.equalTo(ReadReceiptsSummaryEntityFields.ROOM_ID, roomId)
}
return query
} }

View File

@ -22,13 +22,15 @@ import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMo
import io.realm.* import io.realm.*
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun TimelineEventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId) .equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
} }
internal fun TimelineEventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventIds: List<String>): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray()) .`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray())
} }
@ -121,6 +123,6 @@ internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Re
val sendStatesStr = sendStates.map { it.name }.toTypedArray() val sendStatesStr = sendStates.map { it.name }.toTypedArray()
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.`in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR,sendStatesStr) .`in`(TimelineEventEntityFields.ROOT.SEND_STATE_STR, sendStatesStr)
.findAll() .findAll()
} }

View File

@ -85,7 +85,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
EventAnnotationsSummaryEntity.where(realm, event.eventId EventAnnotationsSummaryEntity.where(realm, event.eventId
?: "").findFirst()?.let { ?: "").findFirst()?.let {
TimelineEventEntity.where(realm, eventId = event.eventId TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
?: "").findFirst()?.let { tet -> ?: "").findFirst()?.let { tet ->
tet.annotations = it tet.annotations = it
} }
@ -167,8 +167,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
if (existing == null) { if (existing == null) {
Timber.v("###REPLACE creating new relation summary for $targetEventId") Timber.v("###REPLACE creating new relation summary for $targetEventId")
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId)
existing.roomId = roomId
} }
//we have it //we have it
@ -233,8 +232,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
val eventId = event.eventId ?: "" val eventId = event.eventId ?: ""
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
if (existing == null) { if (existing == null) {
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) val eventSummary = EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
eventSummary.roomId = roomId
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = it.key sum.key = it.key
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
@ -261,7 +259,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
val reactionEventId = event.eventId val reactionEventId = event.eventId
Timber.v("Reaction $reactionEventId relates to $relatedEventID") Timber.v("Reaction $reactionEventId relates to $relatedEventID")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventID).findFirst() val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventID).findFirst()
?: EventAnnotationsSummaryEntity.create(realm, relatedEventID).apply { this.roomId = roomId } ?: EventAnnotationsSummaryEntity.create(realm, roomId, relatedEventID).apply { this.roomId = roomId }
var sum = eventSummary.reactionsSummary.find { it.key == reaction } var sum = eventSummary.reactionsSummary.find { it.key == reaction }
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId

View File

@ -17,9 +17,7 @@
package im.vector.matrix.android.internal.session.room.read package im.vector.matrix.android.internal.session.room.read
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
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.* import im.vector.matrix.android.internal.database.query.*
@ -83,7 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }
if (readReceiptEventId != null if (readReceiptEventId != null
&& !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) { && !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) {
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
Timber.w("Can't set read receipt for local event $readReceiptEventId") Timber.w("Can't set read receipt for local event $readReceiptEventId")
} else { } else {
@ -112,7 +110,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId
if (isLatestReceived) { if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@awaitTransaction ?: return@awaitTransaction
roomSummary.notificationCount = 0 roomSummary.notificationCount = 0
roomSummary.highlightCount = 0 roomSummary.highlightCount = 0
roomSummary.hasUnreadMessages = false roomSummary.hasUnreadMessages = false
@ -121,14 +119,15 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
} }
} }
private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId
val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull() ?: return true
val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst() val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst()
val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst()
val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
eventToCheckIndex > readReceiptIndex val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE
newReadMarkerIndex > currentReadMarkerIndex
} }
} }

View File

@ -158,7 +158,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
override fun deleteFailedEcho(localEcho: TimelineEvent) { override fun deleteFailedEcho(localEcho: TimelineEvent) {
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
it.deleteFromRealm() it.deleteFromRealm()
} }
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {

View File

@ -24,6 +24,7 @@ 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.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.helper.deleteOnCascade
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.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
@ -625,12 +626,14 @@ internal class DefaultTimeline(
} }
private fun clearUnlinkedEvents(realm: Realm) { private fun clearUnlinkedEvents(realm: Realm) {
realm.executeTransaction { realm.executeTransaction { localRealm ->
val unlinkedChunks = ChunkEntity val unlinkedChunks = ChunkEntity
.where(it, roomId = roomId) .where(localRealm, roomId = roomId)
.equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) .equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true)
.findAll() .findAll()
unlinkedChunks.deleteAllFromRealm() unlinkedChunks.forEach {
it.deleteOnCascade()
}
} }
} }

View File

@ -60,14 +60,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
timelineEventMapper, timelineEventMapper,
settings, settings,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
TimelineHiddenReadMarker(roomId) TimelineHiddenReadMarker(roomId, settings)
) )
} }
override fun getTimeLineEvent(eventId: String): TimelineEvent? { override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy return monarchy
.fetchCopyMap({ .fetchCopyMap({
TimelineEventEntity.where(it, eventId = eventId).findFirst() TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
}, { entity, realm -> }, { entity, realm ->
timelineEventMapper.map(entity) timelineEventMapper.map(entity)
}) })
@ -75,7 +75,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent> { override fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent> {
val liveData = RealmLiveData(monarchy.realmConfiguration) { val liveData = RealmLiveData(monarchy.realmConfiguration) {
TimelineEventEntity.where(it, eventId = eventId) TimelineEventEntity.where(it, roomId = roomId, eventId = eventId)
} }
return Transformations.map(liveData) { events -> return Transformations.map(liveData) { events ->
events.firstOrNull()?.let { timelineEventMapper.map(it) } events.firstOrNull()?.let { timelineEventMapper.map(it) }

View File

@ -18,12 +18,16 @@
package im.vector.matrix.android.internal.session.room.timeline package im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
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.FilterContent
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObjectChangeListener import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
/** /**
@ -31,7 +35,8 @@ import io.realm.RealmResults
* When an hidden event has read marker, we want to transfer it on the first older displayed event. * When an hidden event has read marker, we want to transfer it 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. * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
*/ */
internal class TimelineHiddenReadMarker constructor(private val roomId: String) { internal class TimelineHiddenReadMarker constructor(private val roomId: String,
private val settings: TimelineSettings) {
interface Delegate { interface Delegate {
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
@ -39,39 +44,42 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String)
} }
private var previousDisplayedEventId: String? = null private var previousDisplayedEventId: String? = null
private var readMarkerEntity: ReadMarkerEntity? = null private var hiddenReadMarker: RealmResults<ReadMarkerEntity>? = null
private lateinit var liveEvents: RealmResults<TimelineEventEntity> private lateinit var liveEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate private lateinit var delegate: Delegate
private val readMarkerListener = RealmObjectChangeListener<ReadMarkerEntity> { readMarker, _ -> private val readMarkerListener = OrderedRealmCollectionChangeListener<RealmResults<ReadMarkerEntity>> { readMarkers, changeSet ->
if (!readMarker.isLoaded || !readMarker.isValid) { if (!readMarkers.isLoaded || !readMarkers.isValid) {
return@RealmObjectChangeListener return@OrderedRealmCollectionChangeListener
} }
var hasChange = false var hasChange = false
previousDisplayedEventId?.also { if (changeSet.deletions.isNotEmpty()) {
hasChange = delegate.rebuildEvent(it, false) previousDisplayedEventId?.also {
previousDisplayedEventId = null hasChange = delegate.rebuildEvent(it, false)
} previousDisplayedEventId = null
val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null
if (isEventHidden) {
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
?: return@RealmObjectChangeListener
val displayIndex = hiddenEvent.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 rebuild this one with marker
if (firstDisplayedEvent != null) {
previousDisplayedEventId = firstDisplayedEvent.eventId
hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
}
} }
} }
if (hasChange) delegate.onReadMarkerUpdated() val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
?: return@OrderedRealmCollectionChangeListener
val displayIndex = hiddenEvent.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 rebuild this one with marker
if (firstDisplayedEvent != null) {
previousDisplayedEventId = firstDisplayedEvent.eventId
hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
}
}
if (hasChange) {
delegate.onReadMarkerUpdated()
}
} }
@ -83,8 +91,10 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String)
this.delegate = delegate this.delegate = delegate
// We are looking for read receipts set on hidden events. // We are looking for read receipts set on hidden events.
// We only accept those with a timelineEvent (so coming from pagination/sync). // We only accept those with a timelineEvent (so coming from pagination/sync).
readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId)
.findFirstAsync() .isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT)
.filterReceiptsWithSettings()
.findAllAsync()
.also { it.addChangeListener(readMarkerListener) } .also { it.addChangeListener(readMarkerListener) }
} }
@ -93,7 +103,26 @@ internal class TimelineHiddenReadMarker constructor(private val roomId: String)
* Dispose the realm query subscription. Has to be called on an HandlerThread * Dispose the realm query subscription. Has to be called on an HandlerThread
*/ */
fun dispose() { fun dispose() {
this.readMarkerEntity?.removeAllChangeListeners() this.hiddenReadMarker?.removeAllChangeListeners()
} }
/**
* We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
*/
private fun RealmQuery<ReadMarkerEntity>.filterReceiptsWithSettings(): RealmQuery<ReadMarkerEntity> {
beginGroup()
if (settings.filterTypes) {
not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
}
if (settings.filterTypes && settings.filterEdits) {
or()
}
if (settings.filterEdits) {
like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
}
endGroup()
return this
}
} }

View File

@ -16,10 +16,12 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
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.query.getOrCreate 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 io.realm.Realm import io.realm.Realm
@ -37,15 +39,14 @@ internal class RoomFullyReadHandler @Inject constructor() {
RoomSummaryEntity.getOrCreate(realm, roomId).apply { RoomSummaryEntity.getOrCreate(realm, roomId).apply {
readMarkerId = content.eventId readMarkerId = content.eventId
} }
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId) // Remove the old markers if any
// Remove the old marker if any val oldReadMarkerEvents = TimelineEventEntity.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH).isNotNull(TimelineEventEntityFields.READ_MARKER.`$`).findAll()
if (readMarkerEntity.eventId.isNotEmpty()) { oldReadMarkerEvents.forEach { it.readMarker = null }
val oldReadMarkerEvent = TimelineEventEntity.where(realm, eventId = readMarkerEntity.eventId).findFirst() val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
oldReadMarkerEvent?.readMarker = null this.eventId = content.eventId
} }
readMarkerEntity.eventId = content.eventId
// Attach to timelineEvent if known // Attach to timelineEvent if known
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst() val timelineEventEntity = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findFirst()
timelineEventEntity?.readMarker = readMarkerEntity timelineEventEntity?.readMarker = readMarkerEntity
} }

View File

@ -21,16 +21,21 @@ package im.vector.riotx.core.ui.views
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import butterknife.ButterKnife import butterknife.ButterKnife
import com.airbnb.epoxy.VisibilityState
import com.google.android.material.internal.ViewUtils.dpToPx
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.* import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.*
import me.gujun.android.span.span import me.gujun.android.span.span
import me.saket.bettermovementmethod.BetterLinkMovementMethod import me.saket.bettermovementmethod.BetterLinkMovementMethod
import timber.log.Timber
class JumpToReadMarkerView @JvmOverloads constructor( class JumpToReadMarkerView @JvmOverloads constructor(
context: Context, context: Context,
@ -49,26 +54,34 @@ class JumpToReadMarkerView @JvmOverloads constructor(
setupView() setupView()
} }
private var readMarkerId: String? = null
private fun setupView() { private fun setupView() {
LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this) inflate(context, R.layout.view_jump_to_read_marker, this)
setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance() jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance()
isClickable = true isClickable = true
jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) {
textDecorationLine = "underline"
onClick = {
readMarkerId?.also {
callback?.onJumpToReadMarkerClicked(it)
}
}
}
closeJumpToReadMarkerView.setOnClickListener { closeJumpToReadMarkerView.setOnClickListener {
visibility = View.GONE visibility = View.INVISIBLE
callback?.onClearReadMarkerClicked() callback?.onClearReadMarkerClicked()
} }
} }
fun render(show: Boolean, readMarkerId: String?) { fun render(show: Boolean, readMarkerId: String?) {
isVisible = show this.readMarkerId = readMarkerId
if (readMarkerId != null) { visibility = if(show){
jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) { View.VISIBLE
textDecorationLine = "underline" }else {
onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) } View.INVISIBLE
}
} }
} }

View File

@ -45,7 +45,6 @@ class ReadMarkerView @JvmOverloads constructor(
private var callbackDispatcherJob: Job? = null private var callbackDispatcherJob: Job? = null
fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) { fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) {
Timber.v("Bind event $eventId - hasReadMarker: $hasReadMarker - displayReadMarker: $displayReadMarker")
this.eventId = eventId this.eventId = eventId
this.callback = readMarkerCallback this.callback = readMarkerCallback
if (displayReadMarker) { if (displayReadMarker) {

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.di.ScreenScope import im.vector.riotx.core.di.ScreenScope
import im.vector.riotx.core.utils.createBackgroundHandler
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -31,6 +32,7 @@ class ReadMarkerHelper @Inject constructor() {
var callback: Callback? = null var callback: Callback? = null
private var onReadMarkerLongDisplayed = false private var onReadMarkerLongDisplayed = false
private var jumpToReadMarkerVisible = false
private var readMarkerVisible: Boolean = true private var readMarkerVisible: Boolean = true
private var state: RoomDetailViewState? = null private var state: RoomDetailViewState? = null
@ -75,23 +77,20 @@ class ReadMarkerHelper @Inject constructor() {
val nonNullState = this.state ?: return val nonNullState = this.state ?: return
val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId
if (readMarkerId == null) { val newJumpToReadMarkerVisible = if (readMarkerId == null) {
callback?.onJumpToReadMarkerVisibilityUpdate(false, null) false
}
val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId)
if (positionOfReadMarker == null) {
if (nonNullState.timeline?.isLive == true && lastVisibleItem > 0) {
callback?.onJumpToReadMarkerVisibilityUpdate(true, readMarkerId)
} else {
callback?.onJumpToReadMarkerVisibilityUpdate(false, readMarkerId)
}
} else { } else {
if (positionOfReadMarker > lastVisibleItem) { val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId)
callback?.onJumpToReadMarkerVisibilityUpdate(true, readMarkerId) if (positionOfReadMarker == null) {
nonNullState.timeline?.isLive == true && lastVisibleItem > 0
} else { } else {
callback?.onJumpToReadMarkerVisibilityUpdate(false, readMarkerId) positionOfReadMarker > lastVisibleItem
} }
} }
if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) {
jumpToReadMarkerVisible = newJumpToReadMarkerVisible
callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId)
}
} }

View File

@ -157,7 +157,7 @@ class RoomDetailFragment :
} }
} }
/** /**x
* Sanitize the display name. * Sanitize the display name.
* *
* @param displayName the display name to sanitize * @param displayName the display name to sanitize
@ -373,22 +373,22 @@ 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
updateComposerText(defaultContent) updateComposerText(defaultContent)
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)
avatarRenderer.render(event.senderAvatar, avatarRenderer.render(event.senderAvatar,
event.root.senderId ?: "", event.root.senderId ?: "",
event.senderName, event.senderName,
composerLayout.composerRelatedMessageAvatar) composerLayout.composerRelatedMessageAvatar)
composerLayout.expand { composerLayout.expand {
//need to do it here also when not using quick reply //need to do it here also when not using quick reply
focusComposerAndShowKeyboard() focusComposerAndShowKeyboard()
@ -426,9 +426,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))
} }
@ -487,26 +487,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)?.attributes?.informationData?.let { (model as? AbsMessageItem)?.attributes?.informationData?.let {
val eventId = it.eventId val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString())) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString()))
} }
} }
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).attributes.informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
} }
}) })
val touchHelper = ItemTouchHelper(swipeCallback) val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView) touchHelper.attachToRecyclerView(recyclerView)
} }
@ -948,12 +948,19 @@ class RoomDetailFragment :
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
} }
override fun onReadMarkerLongBound(isDisplayed: Boolean) { override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) {
if (isDisplayed) { readMarkerHelper.onReadMarkerLongDisplayed()
readMarkerHelper.onReadMarkerLongDisplayed() val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
if (readMarkerIndex > lastVisibleItemPosition) {
return
}
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val firstVisibleItem = timelineEventController.adapter.getModelAtPosition(firstVisibleItemPosition)
val nextReadMarkerId = when (firstVisibleItem) {
is BaseEventItem -> firstVisibleItem.getEventId()
else -> null
} }
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
if (nextReadMarkerId != null) { if (nextReadMarkerId != null) {
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId)) roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId))
} }

View File

@ -58,7 +58,6 @@ import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.subscribeLogError import im.vector.riotx.core.utils.subscribeLogError
import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.CommandParser
import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.command.ParsedCommand
import im.vector.riotx.features.home.room.detail.timeline.TimelineLayoutManagerHolder
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable import io.reactivex.Observable
@ -72,7 +71,6 @@ import java.util.concurrent.TimeUnit
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState, class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
private val timelineLayoutManagerHolder: TimelineLayoutManagerHolder,
private val userPreferencesProvider: UserPreferencesProvider, private val userPreferencesProvider: UserPreferencesProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val session: Session private val session: Session
@ -117,8 +115,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeEventDisplayedActions() observeEventDisplayedActions()
observeSummaryState() observeSummaryState()
observeDrafts() observeDrafts()
observeReadMarkerVisibility()
observeOwnState()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@ -182,23 +178,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
copy( copy(
// Create a sendMode from a draft and retrieve the TimelineEvent // Create a sendMode from a draft and retrieve the TimelineEvent
sendMode = when (draft) { sendMode = when (draft) {
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
is UserDraft.QUOTE -> { is UserDraft.QUOTE -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.QUOTE(timelineEvent, draft.text) SendMode.QUOTE(timelineEvent, draft.text)
} }
} }
is UserDraft.REPLY -> { is UserDraft.REPLY -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.REPLY(timelineEvent, draft.text) SendMode.REPLY(timelineEvent, draft.text)
} }
} }
is UserDraft.EDIT -> { is UserDraft.EDIT -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.EDIT(timelineEvent, draft.text) SendMode.EDIT(timelineEvent, draft.text)
} }
} }
} ?: SendMode.REGULAR("") } ?: SendMode.REGULAR("")
) )
} }
} }
@ -207,7 +203,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
@ -342,7 +338,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.EDIT -> { is SendMode.EDIT -> {
//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 {
@ -351,13 +347,13 @@ 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 ?: MessageType.MSGTYPE_TEXT, messageContent?.type ?: MessageType.MSGTYPE_TEXT,
action.text, action.text,
action.autoMarkdown) action.autoMarkdown)
} else { } else {
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
@ -368,7 +364,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)
@ -681,7 +677,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state -> private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state ->
room.setReadMarker(action.eventId, callback = object : MatrixCallback<Unit> {}) var readMarkerId = action.eventId
val indexOfEvent = timeline.getIndexOfEvent(readMarkerId)
// force to set the read marker on the next event
if (indexOfEvent != null) {
timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext ->
readMarkerId = eventIdOfNext
}
}
room.setReadMarker(readMarkerId, callback = object : MatrixCallback<Unit> {})
} }
private fun handleMarkAllAsRead() { private fun handleMarkAllAsRead() {
@ -724,22 +728,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
private fun observeReadMarkerVisibility() {
Observable
.combineLatest(
room.rx().liveReadMarker(),
room.rx().liveReadReceipt(),
BiFunction<Optional<String>, Optional<String>, Boolean> { readMarker, readReceipt ->
readMarker.getOrNull() != readReceipt.getOrNull()
}
)
.subscribe {
setState { copy(readMarkerVisible = it) }
}
.disposeOnClear()
}
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline.dispose()
super.onCleared() super.onCleared()

View File

@ -52,8 +52,7 @@ data class RoomDetailViewState(
val tombstoneEvent: Event? = null, val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE, val syncState: SyncState = SyncState.IDLE,
val highlightedEventId: String? = null, val highlightedEventId: String? = null
val readMarkerVisible: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -82,7 +82,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
interface ReadReceiptsCallback { interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerLongBound(isDisplayed: Boolean) fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean)
} }
interface UrlClickCallback { interface UrlClickCallback {
@ -161,7 +161,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
synchronized(modelCache) { synchronized(modelCache) {
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == viewState.highlightedEventId if (modelCache[i]?.eventId == viewState.highlightedEventId
|| modelCache[i]?.eventId == eventIdToHighlight) { || modelCache[i]?.eventId == eventIdToHighlight) {
modelCache[i] = null modelCache[i] = null
} }
} }
@ -232,8 +232,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// 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)
} }
} }
@ -258,18 +258,24 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val date = event.root.localDateTime() val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime() val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, readMarkerVisible, callback).also { // Don't show read marker if it's on first item
val showReadMarker = if (currentPosition == 0 && event.hasReadMarker) {
false
} else {
readMarkerVisible
}
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, showReadMarker, callback).also {
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
} }
val mergedHeaderModel = mergedHeaderItemFactory.create(event, val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent, nextEvent = nextEvent,
items = items, items = items,
addDaySeparator = addDaySeparator, addDaySeparator = addDaySeparator,
readMarkerVisible = readMarkerVisible, readMarkerVisible = readMarkerVisible,
currentPosition = currentPosition, currentPosition = currentPosition,
eventIdToHighlight = eventIdToHighlight, eventIdToHighlight = eventIdToHighlight,
callback = callback callback = callback
) { ) {
requestModelBuild() requestModelBuild()
} }
@ -317,40 +323,23 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
realPosition++ realPosition++
} }
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
val itemCache = modelCache[i] val itemCache = modelCache[i] ?: continue
if (itemCache?.eventId == eventId) { if (itemCache.eventId == eventId) {
return realPosition return realPosition
} }
if (itemCache?.eventModel != null) { if (itemCache.eventModel != null && !mergedHeaderItemFactory.isCollapsed(itemCache.localId)) {
realPosition++ realPosition++
} }
if (itemCache?.mergedHeaderModel != null) { if (itemCache.mergedHeaderModel != null) {
realPosition++ realPosition++
} }
if (itemCache?.formattedDayModel != null) { if (itemCache.formattedDayModel != null) {
realPosition++ realPosition++
} }
} }
return null return null
} }
fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) {
var offsetValue = 0
for (i in 0 until position) {
val itemCache = modelCache[i]
if (itemCache?.eventModel == null) {
offsetValue--
}
if (itemCache?.mergedHeaderModel != null) {
offsetValue++
}
if (itemCache?.formattedDayModel != null) {
offsetValue++
}
}
return modelCache.getOrNull(position - offsetValue)?.eventId
}
fun isLoadingForward() = showingForwardLoader fun isLoadingForward() = showingForwardLoader
private data class CacheItemData( private data class CacheItemData(

View File

@ -1,29 +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.timeline
import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject
@ScreenScope
class TimelineLayoutManagerHolder @Inject constructor() {
lateinit var layoutManager: LinearLayoutManager
}

View File

@ -71,7 +71,8 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
userId = mergedEvent.root.senderId ?: "", userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar, avatarUrl = senderAvatar,
memberName = senderName ?: "", memberName = senderName ?: "",
eventId = mergedEvent.localId localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
) )
mergedData.add(data) mergedData.add(data)
} }

View File

@ -29,6 +29,7 @@ import androidx.core.view.children
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.VisibilityState
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
@ -58,7 +59,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
private val _readMarkerCallback = object : ReadMarkerView.Callback { private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerLongBound(isDisplayed: Boolean) { override fun onReadMarkerLongBound(isDisplayed: Boolean) {
attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
} }
} }
@ -157,6 +158,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
return true return true
} }
override fun getEventId(): String? {
return attributes.informationData.eventId
}
protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
root.isClickable = attributes.informationData.sendState.isSent() root.isClickable = attributes.informationData.sendState.isSent()
val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState

View File

@ -44,10 +44,13 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
holder
holder.leftGuideline.setGuidelineBegin(leftGuideline) holder.leftGuideline.setGuidelineBegin(leftGuideline)
holder.checkableBackground.isChecked = highlighted holder.checkableBackground.isChecked = highlighted
} }
abstract fun getEventId(): String?
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline) val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground) val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)

View File

@ -55,6 +55,10 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
} }
override fun getEventId(): String? {
return informationData.eventId
}
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
class Holder : BaseHolder(STUB_ID) { class Holder : BaseHolder(STUB_ID) {

View File

@ -42,7 +42,7 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
private val _readMarkerCallback = object : ReadMarkerView.Callback { private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerLongBound(isDisplayed: Boolean) { override fun onReadMarkerLongBound(isDisplayed: Boolean) {
attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed)
} }
} }
@ -89,8 +89,14 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
super.unbind(holder) super.unbind(holder)
} }
override fun getEventId(): String? {
return attributes.mergeData.firstOrNull()?.eventId
}
data class Data( data class Data(
val eventId: Long, val localId: Long,
val eventId: String,
val userId: String, val userId: String,
val memberName: String, val memberName: String,
val avatarUrl: String? val avatarUrl: String?

View File

@ -40,7 +40,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
private val _readMarkerCallback = object : ReadMarkerView.Callback { private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerLongBound(isDisplayed: Boolean) { override fun onReadMarkerLongBound(isDisplayed: Boolean) {
attributes.readReceiptsCallback?.onReadMarkerLongBound(isDisplayed) attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
} }
} }
@ -69,6 +69,11 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
super.unbind(holder) super.unbind(holder)
} }
override fun getEventId(): String? {
return attributes.informationData.eventId
}
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
class Holder : BaseHolder(STUB_ID) { class Holder : BaseHolder(STUB_ID) {

View File

@ -124,10 +124,10 @@
android:id="@+id/jumpToReadMarkerView" android:id="@+id/jumpToReadMarkerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" /> app:layout_constraintTop_toBottomOf="@id/syncStateView" />
<im.vector.riotx.core.ui.views.NotificationAreaView <im.vector.riotx.core.ui.views.NotificationAreaView
android:id="@+id/notificationAreaView" android:id="@+id/notificationAreaView"