mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Read marker: start reworking how we manage it [WIP]
This commit is contained in:
parent
c495aa4914
commit
5e07e96bdb
@ -41,8 +41,7 @@ data class TimelineEvent(
|
||||
val isUniqueDisplayName: Boolean,
|
||||
val senderAvatar: String?,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList(),
|
||||
val hasReadMarker: Boolean = false
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
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.ReadReceiptsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
@ -140,7 +139,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
val senderId = event.senderId ?: ""
|
||||
|
||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
|
||||
?: ReadReceiptsSummaryEntity(eventId, roomId)
|
||||
?: ReadReceiptsSummaryEntity(eventId, roomId)
|
||||
|
||||
// Update RR for the sender of a new message with a dummy one
|
||||
|
||||
@ -168,7 +167,6 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
it.roomId = roomId
|
||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
it.readReceipts = readReceiptsSummaryEntity
|
||||
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
|
||||
}
|
||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||
timelineEvents.add(position, eventEntity)
|
||||
@ -176,14 +174,14 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
|
||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||
return when (direction) {
|
||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||
} ?: defaultValue
|
||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||
return when (direction) {
|
||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||
} ?: defaultValue
|
||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||
}
|
||||
return TimelineEvent(
|
||||
root = timelineEventEntity.root?.asDomain()
|
||||
?: Event("", timelineEventEntity.eventId),
|
||||
?: Event("", timelineEventEntity.eventId),
|
||||
annotations = timelineEventEntity.annotations?.asDomain(),
|
||||
localId = timelineEventEntity.localId,
|
||||
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
||||
@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
readReceipts = readReceipts?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList(),
|
||||
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,6 @@
|
||||
package im.vector.matrix.android.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class ReadMarkerEntity(
|
||||
@ -27,8 +25,5 @@ internal open class ReadMarkerEntity(
|
||||
var eventId: String = ""
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("readMarker")
|
||||
val timelineEvent: RealmResults<TimelineEventEntity>? = null
|
||||
|
||||
companion object
|
||||
}
|
||||
|
@ -30,8 +30,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
||||
var isUniqueDisplayName: Boolean = false,
|
||||
var senderAvatar: String? = null,
|
||||
var senderMembershipEvent: EventEntity? = null,
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null,
|
||||
var readMarker: ReadMarkerEntity? = null
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("timelineEvents")
|
||||
|
@ -74,9 +74,8 @@ internal class DefaultTimeline(
|
||||
private val cryptoService: CryptoService,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val settings: TimelineSettings,
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
|
||||
private val hiddenReadMarker: TimelineHiddenReadMarker
|
||||
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
||||
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||
|
||||
private companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||
@ -197,7 +196,6 @@ internal class DefaultTimeline(
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
||||
}
|
||||
hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this)
|
||||
isReady.set(true)
|
||||
}
|
||||
}
|
||||
@ -217,7 +215,6 @@ internal class DefaultTimeline(
|
||||
if (this::filteredEvents.isInitialized) {
|
||||
filteredEvents.removeAllChangeListeners()
|
||||
}
|
||||
hiddenReadMarker.dispose()
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.dispose()
|
||||
}
|
||||
@ -298,7 +295,7 @@ internal class DefaultTimeline(
|
||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||
}
|
||||
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
@ -310,19 +307,7 @@ internal class DefaultTimeline(
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// TimelineHiddenReadMarker.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
te.copy(hasReadMarker = hasReadMarker)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReadMarkerUpdated() {
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
|
@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
|
||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||
return DefaultTimeline(roomId,
|
||||
eventId,
|
||||
monarchy.realmConfiguration,
|
||||
taskExecutor,
|
||||
contextOfEventTask,
|
||||
clearUnlinkedEventsTask,
|
||||
paginationTask,
|
||||
cryptoService,
|
||||
timelineEventMapper,
|
||||
settings,
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||
TimelineHiddenReadMarker(roomId, settings)
|
||||
eventId,
|
||||
monarchy.realmConfiguration,
|
||||
taskExecutor,
|
||||
contextOfEventTask,
|
||||
clearUnlinkedEventsTask,
|
||||
paginationTask,
|
||||
cryptoService,
|
||||
timelineEventMapper,
|
||||
settings,
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,133 +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.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.ReadMarkerEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.FilterContent
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the read marker for hidden events.
|
||||
* 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.
|
||||
*/
|
||||
internal class TimelineHiddenReadMarker constructor(private val roomId: String,
|
||||
private val settings: TimelineSettings) {
|
||||
|
||||
interface Delegate {
|
||||
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
|
||||
fun onReadMarkerUpdated()
|
||||
}
|
||||
|
||||
private var previousDisplayedEventId: String? = null
|
||||
private var hiddenReadMarker: RealmResults<ReadMarkerEntity>? = null
|
||||
|
||||
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var delegate: Delegate
|
||||
|
||||
private val readMarkerListener = OrderedRealmCollectionChangeListener<RealmResults<ReadMarkerEntity>> { readMarkers, changeSet ->
|
||||
if (!readMarkers.isLoaded || !readMarkers.isValid) {
|
||||
return@OrderedRealmCollectionChangeListener
|
||||
}
|
||||
var hasChange = false
|
||||
if (changeSet.deletions.isNotEmpty()) {
|
||||
previousDisplayedEventId?.also {
|
||||
hasChange = delegate.rebuildEvent(it, false)
|
||||
previousDisplayedEventId = null
|
||||
}
|
||||
}
|
||||
val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener
|
||||
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
|
||||
?: return@OrderedRealmCollectionChangeListener
|
||||
|
||||
val isLoaded = nonFilteredEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId)
|
||||
.findFirst() != null
|
||||
|
||||
val displayIndex = hiddenEvent.root?.displayIndex
|
||||
if (isLoaded && displayIndex != null) {
|
||||
// Then we are looking for the first displayable event after the hidden one
|
||||
val firstDisplayedEvent = filteredEvents.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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the realm query subscription. Has to be called on an HandlerThread
|
||||
*/
|
||||
fun start(realm: Realm,
|
||||
filteredEvents: RealmResults<TimelineEventEntity>,
|
||||
nonFilteredEvents: RealmResults<TimelineEventEntity>,
|
||||
delegate: Delegate) {
|
||||
this.filteredEvents = filteredEvents
|
||||
this.nonFilteredEvents = nonFilteredEvents
|
||||
this.delegate = delegate
|
||||
// We are looking for read receipts set on hidden events.
|
||||
// We only accept those with a timelineEvent (so coming from pagination/sync).
|
||||
hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId)
|
||||
.isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT)
|
||||
.filterReceiptsWithSettings()
|
||||
.findAllAsync()
|
||||
.also { it.addChangeListener(readMarkerListener) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the realm query subscription. Has to be called on an HandlerThread
|
||||
*/
|
||||
fun dispose() {
|
||||
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
|
||||
}
|
||||
}
|
@ -16,14 +16,10 @@
|
||||
|
||||
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.database.model.ReadMarkerEntity
|
||||
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.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() {
|
||||
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
||||
readMarkerId = content.eventId
|
||||
}
|
||||
// Remove the old markers if any
|
||||
val oldReadMarkerEvents = TimelineEventEntity
|
||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
||||
.isNotNull(TimelineEventEntityFields.READ_MARKER.`$`)
|
||||
.findAll()
|
||||
|
||||
oldReadMarkerEvents.forEach { it.readMarker = null }
|
||||
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
||||
ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
||||
this.eventId = content.eventId
|
||||
}
|
||||
// Attach to timelineEvent if known
|
||||
val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll()
|
||||
timelineEventEntities.forEach { it.readMarker = readMarkerEntity }
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,3 @@ fun TimelineEvent.canReact(): Boolean {
|
||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||
return root.getClearType() == EventType.MESSAGE && root.sendState == SendState.SYNCED && !root.isRedacted()
|
||||
}
|
||||
|
||||
fun TimelineEvent.displayReadMarker(myUserId: String): Boolean {
|
||||
return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null
|
||||
}
|
||||
|
@ -1,89 +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.core.ui.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import im.vector.riotx.R
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
private const val DELAY_IN_MS = 1_000L
|
||||
|
||||
class ReadMarkerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
interface Callback {
|
||||
fun onReadMarkerLongBound(isDisplayed: Boolean)
|
||||
}
|
||||
|
||||
private var eventId: String? = null
|
||||
private var callback: Callback? = null
|
||||
private var callbackDispatcherJob: Job? = null
|
||||
|
||||
fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) {
|
||||
this.eventId = eventId
|
||||
this.callback = readMarkerCallback
|
||||
if (displayReadMarker) {
|
||||
startAnimation()
|
||||
} else {
|
||||
this.animation?.cancel()
|
||||
this.visibility = INVISIBLE
|
||||
}
|
||||
if (hasReadMarker) {
|
||||
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
|
||||
delay(DELAY_IN_MS)
|
||||
callback?.onReadMarkerLongBound(displayReadMarker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
this.callbackDispatcherJob?.cancel()
|
||||
this.callback = null
|
||||
this.eventId = null
|
||||
this.animation?.cancel()
|
||||
this.visibility = INVISIBLE
|
||||
}
|
||||
|
||||
private fun startAnimation() {
|
||||
if (animation == null) {
|
||||
animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim)
|
||||
animation.startOffset = DELAY_IN_MS / 2
|
||||
animation.duration = DELAY_IN_MS / 2
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
}
|
||||
visibility = VISIBLE
|
||||
animation.start()
|
||||
}
|
||||
}
|
@ -28,49 +28,14 @@ class ReadMarkerHelper @Inject constructor() {
|
||||
lateinit var timelineEventController: TimelineEventController
|
||||
lateinit var layoutManager: LinearLayoutManager
|
||||
var callback: Callback? = null
|
||||
|
||||
private var onReadMarkerLongDisplayed = false
|
||||
private var jumpToReadMarkerVisible = false
|
||||
private var readMarkerVisible: Boolean = true
|
||||
private var state: RoomDetailViewState? = null
|
||||
|
||||
fun readMarkerVisible(): Boolean {
|
||||
return readMarkerVisible
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
onReadMarkerLongDisplayed = false
|
||||
}
|
||||
|
||||
fun onReadMarkerLongDisplayed() {
|
||||
onReadMarkerLongDisplayed = true
|
||||
}
|
||||
|
||||
fun updateWith(newState: RoomDetailViewState) {
|
||||
state = newState
|
||||
checkReadMarkerVisibility()
|
||||
checkJumpToReadMarkerVisibility()
|
||||
}
|
||||
|
||||
fun onTimelineScrolled() {
|
||||
checkJumpToReadMarkerVisibility()
|
||||
}
|
||||
|
||||
private fun checkReadMarkerVisibility() {
|
||||
val nonNullState = this.state ?: return
|
||||
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
|
||||
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
|
||||
readMarkerVisible = if (!onReadMarkerLongDisplayed) {
|
||||
true
|
||||
} else {
|
||||
if (nonNullState.timeline?.isLive == false) {
|
||||
true
|
||||
} else {
|
||||
!(firstVisibleItem == 0 && lastVisibleItem > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkJumpToReadMarkerVisibility() {
|
||||
val nonNullState = this.state ?: return
|
||||
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
|
||||
|
@ -35,13 +35,15 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailAction()
|
||||
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailAction()
|
||||
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailAction()
|
||||
data class SetReadMarkerAction(val eventId: String) : RoomDetailAction()
|
||||
object MarkAllAsRead : RoomDetailAction()
|
||||
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailAction()
|
||||
data class HandleTombstoneEvent(val event: Event) : RoomDetailAction()
|
||||
object AcceptInvite : RoomDetailAction()
|
||||
object RejectInvite : RoomDetailAction()
|
||||
|
||||
object EnterTrackingUnreadMessagesState : RoomDetailAction()
|
||||
object ExitTrackingUnreadMessagesState : RoomDetailAction()
|
||||
|
||||
data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||
data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||
data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction()
|
||||
|
@ -292,6 +292,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||
debouncer.cancelAll()
|
||||
super.onDestroy()
|
||||
}
|
||||
@ -299,6 +300,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
private fun setupJumpToBottomView() {
|
||||
jumpToBottomView.visibility = View.INVISIBLE
|
||||
jumpToBottomView.setOnClickListener {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||
jumpToBottomView.visibility = View.INVISIBLE
|
||||
withState(roomDetailViewModel) { state ->
|
||||
if (state.timeline?.isLive == false) {
|
||||
@ -428,7 +430,6 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
readMarkerHelper.onResume()
|
||||
super.onResume()
|
||||
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId)
|
||||
}
|
||||
@ -484,13 +485,6 @@ class RoomDetailFragment @Inject constructor(
|
||||
recyclerView.adapter = timelineEventController.adapter
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
updateJumpToBottomViewVisibility()
|
||||
}
|
||||
readMarkerHelper.onTimelineScrolled()
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
when (newState) {
|
||||
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||
@ -668,7 +662,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
scrollOnHighlightedEventCallback.timeline = state.timeline
|
||||
timelineEventController.update(state, readMarkerHelper.readMarkerVisible())
|
||||
timelineEventController.update(state)
|
||||
inviteView.visibility = View.GONE
|
||||
val uid = session.myUserId
|
||||
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
||||
@ -1024,28 +1018,8 @@ class RoomDetailFragment @Inject constructor(
|
||||
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
||||
}
|
||||
|
||||
override fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean) {
|
||||
readMarkerHelper.onReadMarkerLongDisplayed()
|
||||
val readMarkerIndex = timelineEventController.searchPositionOfEvent(readMarkerId) ?: return
|
||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||
if (readMarkerIndex > lastVisibleItemPosition) {
|
||||
return
|
||||
}
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
var nextReadMarkerId: String? = null
|
||||
for (itemPosition in firstVisibleItemPosition until lastVisibleItemPosition) {
|
||||
val timelineItem = timelineEventController.adapter.getModelAtPosition(itemPosition)
|
||||
if (timelineItem is BaseEventItem) {
|
||||
val eventId = timelineItem.getEventIds().firstOrNull() ?: continue
|
||||
if (!LocalEcho.isLocalEchoId(eventId)) {
|
||||
nextReadMarkerId = eventId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextReadMarkerId != null) {
|
||||
roomDetailViewModel.handle(RoomDetailAction.SetReadMarkerAction(nextReadMarkerId))
|
||||
}
|
||||
override fun onReadMarkerDisplayed() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
|
||||
}
|
||||
|
||||
// AutocompleteUserPresenter.Callback
|
||||
|
@ -40,6 +40,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.tombstone.RoomTombstoneContent
|
||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
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.getTextEditableContent
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
@ -64,6 +65,7 @@ import org.commonmark.renderer.html.HtmlRenderer
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
||||
userPreferencesProvider: UserPreferencesProvider,
|
||||
@ -102,6 +104,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
// Slot to keep a pending uri during permission request
|
||||
var pendingUri: Uri? = null
|
||||
|
||||
private var trackUnreadMessages = AtomicBoolean(false)
|
||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: RoomDetailViewState): RoomDetailViewModel
|
||||
@ -120,6 +125,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
|
||||
init {
|
||||
getSnapshotOfReadMarkerId()
|
||||
observeSyncState()
|
||||
observeRoomSummary()
|
||||
observeEventDisplayedActions()
|
||||
@ -132,33 +138,47 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
when (action) {
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadFile -> handleDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.SetReadMarkerAction -> handleSetReadMarkerAction(action)
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
|
||||
is RoomDetailAction.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailAction.SendMedia -> handleSendMedia(action)
|
||||
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
|
||||
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
|
||||
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
|
||||
is RoomDetailAction.SendReaction -> handleSendReaction(action)
|
||||
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
|
||||
is RoomDetailAction.RejectInvite -> handleRejectInvite()
|
||||
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
|
||||
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
|
||||
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||
is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action)
|
||||
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
|
||||
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
|
||||
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
|
||||
is RoomDetailAction.DownloadFile -> handleDownloadFile(action)
|
||||
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
|
||||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
is RoomDetailAction.EnterTrackingUnreadMessagesState -> handleEnterTrackingUnreadMessages()
|
||||
is RoomDetailAction.ExitTrackingUnreadMessagesState -> handleExitTrackingUnreadMessages()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEnterTrackingUnreadMessages() {
|
||||
trackUnreadMessages.set(true)
|
||||
}
|
||||
|
||||
private fun handleExitTrackingUnreadMessages() {
|
||||
if (trackUnreadMessages.getAndSet(false)) {
|
||||
mostRecentDisplayedEvent?.root?.eventId?.also {
|
||||
room.setReadMarker(it, callback = object : MatrixCallback<Unit> {})
|
||||
}
|
||||
mostRecentDisplayedEvent = null
|
||||
}
|
||||
}
|
||||
|
||||
@ -685,26 +705,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribeBy(onNext = { actions ->
|
||||
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy
|
||||
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
||||
if (trackUnreadMessages.get()) {
|
||||
if (globalMostRecentDisplayedEvent == null) {
|
||||
mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent
|
||||
} else if (bufferedMostRecentDisplayedEvent.displayIndex > globalMostRecentDisplayedEvent.displayIndex) {
|
||||
mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent
|
||||
}
|
||||
}
|
||||
bufferedMostRecentDisplayedEvent.root.eventId?.let { eventId ->
|
||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
|
||||
}
|
||||
})
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun handleSetReadMarkerAction(action: RoomDetailAction.SetReadMarkerAction) = withState {
|
||||
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() {
|
||||
room.markAllAsRead(object : MatrixCallback<Any> {})
|
||||
}
|
||||
@ -759,6 +775,19 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSnapshotOfReadMarkerId() {
|
||||
room.rx().liveRoomSummary()
|
||||
.unwrap()
|
||||
.filter { it.readMarkerId != null }
|
||||
.take(1)
|
||||
.subscribe { roomSummary ->
|
||||
setState {
|
||||
copy(readMarkerIdSnapshot = roomSummary.readMarkerId)
|
||||
}
|
||||
}
|
||||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun observeSummaryState() {
|
||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||
if (summary.membership == Membership.INVITE) {
|
||||
|
@ -52,7 +52,8 @@ data class RoomDetailViewState(
|
||||
val tombstoneEvent: Event? = null,
|
||||
val tombstoneEventHandling: Async<String> = Uninitialized,
|
||||
val syncState: SyncState = SyncState.IDLE,
|
||||
val highlightedEventId: String? = null
|
||||
val highlightedEventId: String? = null,
|
||||
val readMarkerIdSnapshot: String? = null
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||
|
@ -31,15 +31,10 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotx.core.date.VectorDateFormatter
|
||||
import im.vector.riotx.core.epoxy.LoadingItem_
|
||||
import im.vector.riotx.core.extensions.localDateTime
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
|
||||
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
||||
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
import im.vector.riotx.features.media.VideoContentRenderer
|
||||
@ -50,8 +45,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private val timelineItemFactory: TimelineItemFactory,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val dimensionConverter: DimensionConverter,
|
||||
@TimelineEventControllerHandler
|
||||
private val backgroundHandler: Handler
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||
@ -86,7 +79,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
|
||||
interface ReadReceiptsCallback {
|
||||
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
|
||||
fun onReadMarkerLongBound(readMarkerId: String, isDisplayed: Boolean)
|
||||
fun onReadMarkerDisplayed()
|
||||
}
|
||||
|
||||
interface UrlClickCallback {
|
||||
@ -101,6 +94,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var timeline: Timeline? = null
|
||||
private var readMarkerIdSnapshot: String? = null
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
@ -163,7 +157,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
}
|
||||
|
||||
fun update(viewState: RoomDetailViewState, readMarkerVisible: Boolean) {
|
||||
fun update(viewState: RoomDetailViewState) {
|
||||
if (timeline != viewState.timeline) {
|
||||
timeline = viewState.timeline
|
||||
timeline?.listener = this
|
||||
@ -188,8 +182,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
eventIdToHighlight = viewState.highlightedEventId
|
||||
requestModelBuild = true
|
||||
}
|
||||
if (this.readMarkerVisible != readMarkerVisible) {
|
||||
this.readMarkerVisible = readMarkerVisible
|
||||
if (this.readMarkerIdSnapshot != viewState.readMarkerIdSnapshot) {
|
||||
this.readMarkerIdSnapshot = viewState.readMarkerIdSnapshot
|
||||
requestModelBuild = true
|
||||
}
|
||||
if (requestModelBuild) {
|
||||
@ -197,7 +191,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
}
|
||||
|
||||
private var readMarkerVisible: Boolean = false
|
||||
private var eventIdToHighlight: String? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
@ -247,42 +240,40 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
synchronized(modelCache) {
|
||||
val readMarkerIdSnapshot = this.readMarkerIdSnapshot
|
||||
val displayableReadMarkerId = if (readMarkerIdSnapshot != null) {
|
||||
timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
// 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 additional models
|
||||
// We then are sure we always have items up to date.
|
||||
if (modelCache[position] == null
|
||||
|| modelCache[position]?.mergedHeaderModel != null
|
||||
|| modelCache[position]?.formattedDayModel != null) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
if (modelCache[position] == null || modelCache[position]?.hasAdditionalModel() == true) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot, displayableReadMarkerId)
|
||||
}
|
||||
}
|
||||
return modelCache
|
||||
.map {
|
||||
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
|
||||
null
|
||||
} else {
|
||||
it.eventModel
|
||||
}
|
||||
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||
}
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
}
|
||||
return modelCache
|
||||
.map {
|
||||
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
|
||||
null
|
||||
} else {
|
||||
it.eventModel
|
||||
}
|
||||
listOf(it?.readMarkerModel, eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||
}
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>, displayableReadMarkerId: String?): CacheItemData {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextOrNull(currentPosition)
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
// 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 {
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
@ -290,7 +281,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
nextEvent = nextEvent,
|
||||
items = items,
|
||||
addDaySeparator = addDaySeparator,
|
||||
readMarkerVisible = readMarkerVisible,
|
||||
currentPosition = currentPosition,
|
||||
eventIdToHighlight = eventIdToHighlight,
|
||||
callback = callback
|
||||
@ -298,8 +288,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
requestModelBuild()
|
||||
}
|
||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
||||
val readMarkerItem = buildReadMarkerItem(currentPosition, event, displayableReadMarkerId)
|
||||
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem)
|
||||
}
|
||||
|
||||
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
|
||||
private fun buildReadMarkerItem(currentPosition: Int, event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? {
|
||||
return if (currentPosition != 0 && event.root.eventId == displayableReadMarkerId) {
|
||||
TimelineReadMarkerItem_()
|
||||
.also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? {
|
||||
@ -342,6 +344,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val eventId: String?,
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null
|
||||
)
|
||||
val formattedDayModel: DaySeparatorItem? = null,
|
||||
val readMarkerModel: TimelineReadMarkerItem? = null
|
||||
) {
|
||||
fun hasAdditionalModel() = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,6 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
readMarkerVisible: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
exception: Exception? = null): DefaultItem {
|
||||
val text = if (exception == null) {
|
||||
@ -54,7 +53,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
||||
} else {
|
||||
"an exception occurred when rendering the event ${event.root.eventId}"
|
||||
}
|
||||
val informationData = informationDataFactory.create(event, null, readMarkerVisible)
|
||||
val informationData = informationDataFactory.create(event, null)
|
||||
return create(text, informationData, highlight, callback)
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
readMarkerVisible: Boolean,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
@ -66,7 +65,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||
|
||||
// TODO This is not correct format for error, change it
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible)
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
val attributes = attributesFactory.create(null, informationData, callback)
|
||||
return MessageTextItem_()
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
|
@ -36,7 +36,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||
nextEvent: TimelineEvent?,
|
||||
items: List<TimelineEvent>,
|
||||
addDaySeparator: Boolean,
|
||||
readMarkerVisible: Boolean,
|
||||
currentPosition: Int,
|
||||
eventIdToHighlight: String?,
|
||||
callback: TimelineEventController.Callback?,
|
||||
@ -50,20 +49,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||
null
|
||||
} else {
|
||||
var highlighted = false
|
||||
var readMarkerId: String? = null
|
||||
var showReadMarker = false
|
||||
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
||||
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
||||
mergedEvents.forEach { mergedEvent ->
|
||||
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||
highlighted = true
|
||||
}
|
||||
if (readMarkerId == null && mergedEvent.hasReadMarker) {
|
||||
readMarkerId = mergedEvent.root.eventId
|
||||
}
|
||||
if (!showReadMarker && mergedEvent.hasReadMarker && readMarkerVisible) {
|
||||
showReadMarker = true
|
||||
}
|
||||
val senderAvatar = mergedEvent.senderAvatar
|
||||
val senderName = mergedEvent.getDisambiguatedDisplayName()
|
||||
val data = MergedHeaderItem.Data(
|
||||
@ -96,8 +87,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||
mergeItemCollapseStates[event.localId] = it
|
||||
requestModelBuild()
|
||||
},
|
||||
readMarkerId = readMarkerId,
|
||||
showReadMarker = isCollapsed && showReadMarker,
|
||||
readReceiptsCallback = callback
|
||||
)
|
||||
MergedHeaderItem_()
|
||||
|
@ -69,12 +69,11 @@ class MessageItemFactory @Inject constructor(
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
highlight: Boolean,
|
||||
readMarkerVisible: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent, readMarkerVisible)
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.isRedacted()) {
|
||||
// message is redacted
|
||||
@ -91,7 +90,7 @@ class MessageItemFactory @Inject constructor(
|
||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||
) {
|
||||
// This is an edit event, we should it when debugging as a notice event
|
||||
return noticeItemFactory.create(event, highlight, readMarkerVisible, callback)
|
||||
return noticeItemFactory.create(event, highlight, callback)
|
||||
}
|
||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||
|
||||
|
@ -34,10 +34,9 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
readMarkerVisible: Boolean,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val informationData = informationDataFactory.create(event, null, readMarkerVisible)
|
||||
val informationData = informationDataFactory.create(event, null)
|
||||
val attributes = NoticeItem.Attributes(
|
||||
avatarRenderer = avatarRenderer,
|
||||
informationData = informationData,
|
||||
|
@ -33,14 +33,13 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
eventIdToHighlight: String?,
|
||||
readMarkerVisible: Boolean,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||
val highlight = event.root.eventId == eventIdToHighlight
|
||||
|
||||
val computedModel = try {
|
||||
when (event.root.getClearType()) {
|
||||
EventType.STICKER,
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_NAME,
|
||||
@ -53,21 +52,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
EventType.CALL_ANSWER,
|
||||
EventType.REACTION,
|
||||
EventType.REDACTION,
|
||||
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, readMarkerVisible, callback)
|
||||
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
|
||||
// State room create
|
||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||
// Crypto
|
||||
EventType.ENCRYPTED -> {
|
||||
if (event.root.isRedacted()) {
|
||||
// Redacted event, let the MessageItemFactory handle it
|
||||
messageItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
|
||||
messageItemFactory.create(event, nextEvent, highlight, callback)
|
||||
} else {
|
||||
encryptedItemFactory.create(event, nextEvent, highlight, readMarkerVisible, callback)
|
||||
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// Unhandled event types (yet)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, readMarkerVisible, callback)
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE -> defaultItemFactory.create(event, highlight, callback)
|
||||
else -> {
|
||||
Timber.v("Type ${event.root.getClearType()} not handled")
|
||||
null
|
||||
@ -75,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "failed to create message item")
|
||||
defaultItemFactory.create(event, highlight, readMarkerVisible, callback, e)
|
||||
defaultItemFactory.create(event, highlight, callback, e)
|
||||
}
|
||||
return (computedModel ?: EmptyItem_())
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val colorProvider: ColorProvider) {
|
||||
|
||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?, readMarkerVisible: Boolean): MessageInformationData {
|
||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
||||
// Non nullability has been tested before
|
||||
val eventId = event.root.eventId!!
|
||||
|
||||
@ -63,8 +63,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
|
||||
}
|
||||
|
||||
val displayReadMarker = readMarkerVisible && event.hasReadMarker
|
||||
|
||||
return MessageInformationData(
|
||||
eventId = eventId,
|
||||
senderId = event.root.senderId ?: "",
|
||||
@ -88,9 +86,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
.map {
|
||||
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
|
||||
}
|
||||
.toList(),
|
||||
hasReadMarker = event.hasReadMarker,
|
||||
displayReadMarker = displayReadMarker
|
||||
.toList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,19 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?)
|
||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
||||
private var dispatched: Boolean = false
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE && !dispatched) {
|
||||
dispatched = true
|
||||
callback?.onReadMarkerDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||
private val event: TimelineEvent)
|
||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
@ -27,7 +27,6 @@ import com.airbnb.epoxy.EpoxyAttribute
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
@ -50,13 +49,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
||||
})
|
||||
|
||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
||||
|
||||
override fun onReadMarkerLongBound(isDisplayed: Boolean) {
|
||||
attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
|
||||
}
|
||||
}
|
||||
|
||||
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
|
||||
override fun onReacted(reactionButton: ReactionButton) {
|
||||
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true)
|
||||
@ -110,12 +102,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
attributes.avatarRenderer,
|
||||
_readReceiptsClickListener
|
||||
)
|
||||
holder.readMarkerView.bindView(
|
||||
attributes.informationData.eventId,
|
||||
attributes.informationData.hasReadMarker,
|
||||
attributes.informationData.displayReadMarker,
|
||||
_readMarkerCallback
|
||||
)
|
||||
|
||||
val reactions = attributes.informationData.orderedReactionList
|
||||
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
|
||||
@ -138,7 +124,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
}
|
||||
|
||||
override fun unbind(holder: H) {
|
||||
holder.readMarkerView.unbind()
|
||||
holder.readReceiptsView.unbind()
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.platform.CheckableView
|
||||
import im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
|
||||
@ -62,7 +61,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
||||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
super.bindView(itemView)
|
||||
|
@ -25,7 +25,6 @@ import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@ -39,13 +38,6 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
||||
attributes.mergeData.distinctBy { it.userId }
|
||||
}
|
||||
|
||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
||||
|
||||
override fun onReadMarkerLongBound(isDisplayed: Boolean) {
|
||||
attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.readMarkerId ?: "", isDisplayed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
@ -77,16 +69,6 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
holder.readMarkerView.bindView(
|
||||
attributes.readMarkerId,
|
||||
!attributes.readMarkerId.isNullOrEmpty(),
|
||||
attributes.showReadMarker,
|
||||
_readMarkerCallback)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.readMarkerView.unbind()
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
override fun getEventIds(): List<String> {
|
||||
@ -102,9 +84,7 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
||||
)
|
||||
|
||||
data class Attributes(
|
||||
val readMarkerId: String?,
|
||||
val isCollapsed: Boolean,
|
||||
val showReadMarker: Boolean,
|
||||
val mergeData: List<Data>,
|
||||
val avatarRenderer: AvatarRenderer,
|
||||
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
|
@ -33,9 +33,7 @@ data class MessageInformationData(
|
||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||
val hasBeenEdited: Boolean = false,
|
||||
val hasPendingEdits: Boolean = false,
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val hasReadMarker: Boolean = false,
|
||||
val displayReadMarker: Boolean = false
|
||||
val readReceipts: List<ReadReceiptData> = emptyList()
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
|
@ -22,7 +22,6 @@ import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
@ -37,12 +36,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
||||
})
|
||||
|
||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
||||
|
||||
override fun onReadMarkerLongBound(isDisplayed: Boolean) {
|
||||
attributes.readReceiptsCallback?.onReadMarkerLongBound(attributes.informationData.eventId, isDisplayed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
@ -56,17 +49,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
)
|
||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||
holder.readMarkerView.bindView(
|
||||
attributes.informationData.eventId,
|
||||
attributes.informationData.hasReadMarker,
|
||||
attributes.informationData.displayReadMarker,
|
||||
_readMarkerCallback
|
||||
)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.readMarkerView.unbind()
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
override fun getEventIds(): List<String> {
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_read_marker)
|
||||
abstract class TimelineReadMarkerItem : VectorEpoxyModel<TimelineReadMarkerItem.Holder>() {
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder()
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
android:id="@+id/messageSelectedBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignBottom="@+id/readMarkerView"
|
||||
android:layout_alignBottom="@+id/informationBottom"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="?riotx_highlighted_message_background" />
|
||||
|
||||
@ -145,15 +145,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
android:id="@+id/readMarkerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:layout_below="@+id/informationBottom"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="?attr/vctr_unread_marker_line_color"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</RelativeLayout>
|
@ -53,31 +53,13 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/informationBottom"
|
||||
android:layout_width="match_parent"
|
||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
android:id="@+id/readReceiptsView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/viewStubContainer"
|
||||
android:orientation="vertical">
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
android:id="@+id/readReceiptsView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
android:id="@+id/readMarkerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="?attr/vctr_unread_marker_line_color"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</LinearLayout>
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
</RelativeLayout>
|
18
vector/src/main/res/layout/item_timeline_read_marker.xml
Normal file
18
vector/src/main/res/layout/item_timeline_read_marker.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?attr/riotx_header_panel_background"
|
||||
android:gravity="center"
|
||||
android:minHeight="32dp"
|
||||
android:text="Unread messages"
|
||||
android:textColor="?riotx_text_primary" />
|
||||
|
||||
</FrameLayout>
|
Loading…
Reference in New Issue
Block a user