mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Timeline: fix permalink towards an hidden event
This commit is contained in:
parent
d1ff3314a7
commit
90eeb68d36
@ -44,7 +44,6 @@ interface Timeline {
|
||||
*/
|
||||
fun dispose()
|
||||
|
||||
|
||||
fun restartWithEventId(eventId: String?)
|
||||
|
||||
|
||||
@ -73,8 +72,9 @@ interface Timeline {
|
||||
|
||||
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
|
||||
|
||||
fun getFirstDisplayableEventId(eventId: String): String?
|
||||
|
||||
interface Listener {
|
||||
interface Listener {
|
||||
/**
|
||||
* Call when the timeline has been updated through pagination or sync.
|
||||
* @param snapshot the most uptodate snapshot
|
||||
|
@ -99,7 +99,8 @@ internal class DefaultTimeline(
|
||||
private val cancelableBag = CancelableBag()
|
||||
private val debouncer = Debouncer(mainHandler)
|
||||
|
||||
private lateinit var liveEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
|
||||
|
||||
private var roomEntity: RoomEntity? = null
|
||||
@ -128,9 +129,9 @@ internal class DefaultTimeline(
|
||||
}
|
||||
changeSet.insertionRanges.forEach { range ->
|
||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||
Pair(liveEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||
Pair(filteredEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||
} else {
|
||||
Pair(liveEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||
Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
val state = getPaginationState(direction)
|
||||
if (state.isPaginating) {
|
||||
@ -218,9 +219,9 @@ internal class DefaultTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
liveEvents = buildEventQuery(realm)
|
||||
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||
filteredEvents = nonFilteredEvents.where()
|
||||
.filterEventsWithSettings()
|
||||
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.findAllAsync()
|
||||
.also { it.addChangeListener(eventsChangeListener) }
|
||||
|
||||
@ -229,9 +230,9 @@ internal class DefaultTimeline(
|
||||
.also { it.addChangeListener(relationsListener) }
|
||||
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.start(realm, liveEvents, this)
|
||||
hiddenReadReceipts.start(realm, filteredEvents, this)
|
||||
}
|
||||
hiddenReadMarker.start(realm, liveEvents, this)
|
||||
hiddenReadMarker.start(realm, filteredEvents, this)
|
||||
isReady.set(true)
|
||||
}
|
||||
}
|
||||
@ -246,7 +247,7 @@ internal class DefaultTimeline(
|
||||
BACKGROUND_HANDLER.post {
|
||||
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
||||
eventRelations.removeAllChangeListeners()
|
||||
liveEvents.removeAllChangeListeners()
|
||||
filteredEvents.removeAllChangeListeners()
|
||||
hiddenReadMarker.dispose()
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.dispose()
|
||||
@ -267,25 +268,56 @@ internal class DefaultTimeline(
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
override fun getIndexOfEvent(eventId: String?): Int? {
|
||||
return builtEventsIdMap[eventId]
|
||||
}
|
||||
|
||||
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
|
||||
return builtEvents.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun getIndexOfEvent(eventId: String?): Int? {
|
||||
return builtEventsIdMap[eventId]
|
||||
}
|
||||
|
||||
override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
|
||||
return builtEventsIdMap[eventId]?.let {
|
||||
getTimelineEventAtIndex(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFirstDisplayableEventId(eventId: String): String? {
|
||||
// If the item is built, the id is obviously displayable
|
||||
val builtIndex = builtEventsIdMap[eventId]
|
||||
if (builtIndex != null) {
|
||||
return eventId
|
||||
}
|
||||
// Otherwise, we should check if the event is in the db, but is hidden because of filters
|
||||
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
||||
val nonFilteredEvents = buildEventQuery(localRealm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||
val nonFilteredEvent = nonFilteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst()
|
||||
val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll()
|
||||
val isEventInDb = nonFilteredEvent != null
|
||||
|
||||
val isHidden = isEventInDb && filteredEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() == null
|
||||
if (isHidden) {
|
||||
val displayIndex = nonFilteredEvent?.root?.displayIndex
|
||||
if (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()
|
||||
firstDisplayedEvent?.eventId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||
}
|
||||
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
@ -297,7 +329,7 @@ internal class DefaultTimeline(
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// TimelineHiddenReadMarker.Delegate
|
||||
// TimelineHiddenReadMarker.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
@ -309,7 +341,7 @@ internal class DefaultTimeline(
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
@ -415,22 +447,23 @@ internal class DefaultTimeline(
|
||||
*/
|
||||
private fun handleInitialLoad() {
|
||||
var shouldFetchInitialEvent = false
|
||||
val initialDisplayIndex = if (initialEventId == null) {
|
||||
liveEvents.firstOrNull()?.root?.displayIndex
|
||||
val currentInitialEventId = initialEventId
|
||||
val initialDisplayIndex = if (currentInitialEventId == null) {
|
||||
filteredEvents.firstOrNull()?.root?.displayIndex
|
||||
} else {
|
||||
val initialEvent = liveEvents.where()
|
||||
val initialEvent = nonFilteredEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
|
||||
.findFirst()
|
||||
|
||||
shouldFetchInitialEvent = initialEvent == null
|
||||
initialEvent?.root?.displayIndex
|
||||
}
|
||||
prevDisplayIndex = initialDisplayIndex
|
||||
nextDisplayIndex = initialDisplayIndex
|
||||
val currentInitialEventId = initialEventId
|
||||
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
||||
fetchEvent(currentInitialEventId)
|
||||
} else {
|
||||
val count = min(settings.initialSize, liveEvents.size)
|
||||
val count = min(settings.initialSize, filteredEvents.size)
|
||||
if (initialEventId == null) {
|
||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
|
||||
} else {
|
||||
@ -494,7 +527,7 @@ internal class DefaultTimeline(
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun getLiveChunk(): ChunkEntity? {
|
||||
return liveEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||
return filteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -552,7 +585,7 @@ internal class DefaultTimeline(
|
||||
direction: Timeline.Direction,
|
||||
count: Long,
|
||||
strict: Boolean): RealmResults<TimelineEventEntity> {
|
||||
val offsetQuery = liveEvents.where()
|
||||
val offsetQuery = filteredEvents.where()
|
||||
if (direction == Timeline.Direction.BACKWARDS) {
|
||||
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
if (strict) {
|
||||
@ -631,7 +664,7 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
|
||||
// Extension methods ***************************************************************************
|
||||
// Extension methods ***************************************************************************
|
||||
|
||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||
|
@ -112,7 +112,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val nextToken: String?
|
||||
val prevToken: String?
|
||||
@ -141,7 +141,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
@ -163,8 +163,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
|
||||
} else {
|
||||
val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
|
||||
ChunkEntity
|
||||
.findAllIncludingEvents(realm, newEventIds)
|
||||
val overlappedChunks = ChunkEntity.findAllIncludingEvents(realm, newEventIds)
|
||||
overlappedChunks
|
||||
.filter { it != currentChunk }
|
||||
.forEach { overlapped ->
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
|
||||
@ -194,7 +194,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
|
||||
// We always merge the bottom chunk into top chunk, so we are always merging backwards
|
||||
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
|
||||
return if (direction == PaginationDirection.BACKWARDS) {
|
||||
return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
|
||||
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
|
||||
roomEntity.deleteOnCascade(otherChunk)
|
||||
currentChunk
|
||||
|
@ -702,6 +702,7 @@ class RoomDetailFragment :
|
||||
val summary = state.asyncRoomSummary()
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
scrollOnHighlightedEventCallback.timeline = state.timeline
|
||||
timelineEventController.update(state)
|
||||
inviteView.visibility = View.GONE
|
||||
val uid = session.myUserId
|
||||
|
@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
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.relation.ReactionContent
|
||||
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
|
||||
@ -613,18 +614,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) {
|
||||
val targetEventId = action.eventId
|
||||
val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
|
||||
val targetEventId: String = action.eventId
|
||||
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
|
||||
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
|
||||
if (indexOfEvent == null) {
|
||||
// Event is not already in RAM
|
||||
timeline.restartWithEventId(targetEventId)
|
||||
}
|
||||
if (action.highlight) {
|
||||
setState { copy(highlightedEventId = targetEventId) }
|
||||
setState { copy(highlightedEventId = correctedEventId) }
|
||||
}
|
||||
_navigateToEvent.postLiveEvent(targetEventId)
|
||||
_navigateToEvent.postLiveEvent(correctedEventId)
|
||||
}
|
||||
|
||||
private fun handleResendEvent(action: RoomDetailActions.ResendMessage) {
|
||||
@ -683,17 +684,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
|
||||
private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state ->
|
||||
var readMarkerId = action.eventId
|
||||
if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) {
|
||||
val indexOfEvent = timeline.getIndexOfEvent(action.eventId)
|
||||
// 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> {})
|
||||
room.setReadMarker(action.eventId, callback = object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
private fun handleMarkAllAsRead() {
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.riotx.features.home.room.detail
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.riotx.core.platform.DefaultListUpdateCallback
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import timber.log.Timber
|
||||
@ -27,9 +28,13 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa
|
||||
|
||||
private val scheduledEventId = AtomicReference<String?>()
|
||||
|
||||
var timeline: Timeline? = null
|
||||
|
||||
override fun onChanged(position: Int, count: Int, tag: Any?) {
|
||||
val eventId = scheduledEventId.get() ?: return
|
||||
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
|
||||
val nonNullTimeline = timeline ?: return
|
||||
val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId)
|
||||
val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId)
|
||||
if (positionToScroll != null) {
|
||||
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
|
@ -296,8 +296,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
}
|
||||
|
||||
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
|
||||
fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) {
|
||||
// Search in the cache
|
||||
if (eventId == null) {
|
||||
return null
|
||||
}
|
||||
var realPosition = 0
|
||||
if (showingForwardLoader) {
|
||||
realPosition++
|
||||
|
Loading…
Reference in New Issue
Block a user