Merge branch 'develop' into feature/bma/cleanup

This commit is contained in:
Benoit Marty 2021-03-31 17:55:50 +02:00 committed by GitHub
commit 295be5286b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 683 additions and 813 deletions

View file

@ -13,6 +13,7 @@ Improvements 🙌:
- Api interceptor to allow app developers peek responses (#2986)
- Update reactions to Unicode 13.1 (#2998)
- Be more robust when parsing some enums
- Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior)
Bugfix 🐛:
- Fix bad theme change for the MainActivity
@ -24,6 +25,7 @@ Translations 🗣:
SDK API changes ⚠️:
- Several Services have been migrated to coroutines (#2449)
- Removes filtering options on Timeline.
Build 🧱:
-

View file

@ -95,12 +95,6 @@ interface Timeline {
*/
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
/**
* Returns the first displayable events starting from eventId.
* It does depend on the provided [TimelineSettings].
*/
fun getFirstDisplayableEventId(eventId: String): String?
interface Listener {
/**
* Call when the timeline has been updated through pagination or sync.

View file

@ -24,10 +24,6 @@ data class TimelineSettings(
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
*/
val initialSize: Int,
/**
* Filters for timeline event
*/
val filters: TimelineEventFilters = TimelineEventFilters(),
/**
* If true, will build read receipts for each event.
*/

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
@ -25,9 +24,9 @@ import javax.inject.Inject
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true): TimelineEvent {
val readReceipts = if (buildReadReceipts) {
correctedReadReceipts ?: timelineEventEntity.readReceipts
timelineEventEntity.readReceipts
?.let {
readReceiptsSummaryMapper.map(it)
}

View file

@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -70,14 +69,12 @@ internal class DefaultTimeline(
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val timelineInput: TimelineInput,
private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler
) : Timeline,
TimelineHiddenReadReceipts.Delegate,
TimelineInput.Listener,
UIEchoManager.Listener {
@ -93,8 +90,7 @@ internal class DefaultTimeline(
private val cancelableBag = CancelableBag()
private val debouncer = Debouncer(mainHandler)
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
private lateinit var timelineEvents: RealmResults<TimelineEventEntity>
private lateinit var sendingEvents: RealmResults<TimelineEventEntity>
private var prevDisplayIndex: Int? = null
@ -168,16 +164,9 @@ internal class DefaultTimeline(
postSnapshot()
}
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings(settings)
.findAll()
nonFilteredEvents.addChangeListener(eventsChangeListener)
timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
timelineEvents.addChangeListener(eventsChangeListener)
handleInitialLoad()
if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
}
loadRoomMembersTask
.configureWith(LoadRoomMembersTask.Params(roomId)) {
this.callback = NoOpMatrixCallback()
@ -205,10 +194,6 @@ internal class DefaultTimeline(
}
}
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
}
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
isReady.set(false)
@ -220,11 +205,8 @@ internal class DefaultTimeline(
if (this::sendingEvents.isInitialized) {
sendingEvents.removeAllChangeListeners()
}
if (this::nonFilteredEvents.isInitialized) {
nonFilteredEvents.removeAllChangeListeners()
}
if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.dispose()
if (this::timelineEvents.isInitialized) {
timelineEvents.removeAllChangeListeners()
}
clearAllValues()
backgroundRealm.getAndSet(null).also {
@ -256,48 +238,6 @@ internal class DefaultTimeline(
}
}
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 realmSessionProvider.withRealm { localRealm ->
val nonFilteredEvents = buildEventQuery(localRealm)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll()
val nonFilteredEvent = nonFilteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
.findFirst()
val filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings(settings)
.findAll()
val isEventInDb = nonFilteredEvent != null
val isHidden = isEventInDb && filteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
.findFirst() == null
if (isHidden) {
val displayIndex = nonFilteredEvent?.displayIndex
if (displayIndex != null) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = filteredEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
.findFirst()
firstDisplayedEvent?.eventId
} else {
null
}
} else {
null
}
}
}
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
@ -319,18 +259,6 @@ internal class DefaultTimeline(
listeners.clear()
}
// TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(readReceipts = readReceipts)
}
}
override fun onReadReceiptsUpdated() {
postSnapshot()
}
override fun onNewTimelineEvents(roomId: String, eventIds: List<String>) {
if (isLive && this.roomId == roomId) {
listeners.forEach {
@ -341,18 +269,13 @@ internal class DefaultTimeline(
override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
if (roomId != this.roomId || !isLive) return
val postSnapShot = uiEchoManager.onLocalEchoCreated(timelineEvent)
if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) {
listeners.forEach {
uiEchoManager.onLocalEchoCreated(timelineEvent)
listeners.forEach {
tryOrNull {
it.onNewTimelineEvents(listOf(timelineEvent.eventId))
}
}
if (postSnapShot) {
postSnapshot()
}
postSnapshot()
}
override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) {
@ -439,23 +362,21 @@ internal class DefaultTimeline(
val builtSendingEvents = mutableListOf<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
uiEchoManager.getInMemorySendingEvents()
.filterSendingEventsTo(builtSendingEvents)
.updateWithUiEchoInto(builtSendingEvents)
sendingEvents
.filter { timelineEvent ->
builtSendingEvents.none { it.eventId == timelineEvent.eventId }
}
.map { timelineEventMapper.map(it) }
.filterSendingEventsTo(builtSendingEvents)
.updateWithUiEchoInto(builtSendingEvents)
}
return builtSendingEvents
}
private fun List<TimelineEvent>.filterSendingEventsTo(target: MutableList<TimelineEvent>) {
private fun List<TimelineEvent>.updateWithUiEchoInto(target: MutableList<TimelineEvent>) {
target.addAll(
// Filter out sending event that are not displayable!
filterEventsWithSettings(settings)
// Get most up to date send state (in memory)
.map { uiEchoManager.updateSentStateWithUiEcho(it) }
// Get most up to date send state (in memory)
map { uiEchoManager.updateSentStateWithUiEcho(it) }
)
}
@ -465,14 +386,14 @@ internal class DefaultTimeline(
private fun getState(direction: Timeline.Direction): TimelineState {
return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get()
}
}
private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) {
val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState
}
val currentValue = stateReference.get()
@ -487,9 +408,9 @@ internal class DefaultTimeline(
var shouldFetchInitialEvent = false
val currentInitialEventId = initialEventId
val initialDisplayIndex = if (currentInitialEventId == null) {
nonFilteredEvents.firstOrNull()?.displayIndex
timelineEvents.firstOrNull()?.displayIndex
} else {
val initialEvent = nonFilteredEvents.where()
val initialEvent = timelineEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
.findFirst()
@ -501,7 +422,7 @@ internal class DefaultTimeline(
if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId)
} else {
val count = filteredEvents.size.coerceAtMost(settings.initialSize)
val count = timelineEvents.size.coerceAtMost(settings.initialSize)
if (initialEventId == null) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else {
@ -541,8 +462,7 @@ internal class DefaultTimeline(
val eventEntity = results[index]
eventEntity?.eventId?.let { eventId ->
postSnapshot = rebuildEvent(eventId) {
val builtEvent = buildTimelineEvent(eventEntity)
listOf(builtEvent).filterEventsWithSettings(settings).firstOrNull()
buildTimelineEvent(eventEntity)
} || postSnapshot
}
}
@ -563,9 +483,9 @@ internal class DefaultTimeline(
// We are in the case where event exists, but we do not know the token.
// Fetch (again) the last event to get a token
val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) {
nonFilteredEvents.firstOrNull()?.eventId
timelineEvents.firstOrNull()?.eventId
} else {
nonFilteredEvents.lastOrNull()?.eventId
timelineEvents.lastOrNull()?.eventId
}
if (lastKnownEventId == null) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
@ -636,7 +556,7 @@ internal class DefaultTimeline(
* Return the current Chunk
*/
private fun getLiveChunk(): ChunkEntity? {
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
return timelineEvents.firstOrNull()?.chunk?.firstOrNull()
}
/**
@ -680,14 +600,13 @@ internal class DefaultTimeline(
val time = System.currentTimeMillis() - start
Timber.v("Built ${offsetResults.size} items from db in $time ms")
// For the case where wo reach the lastForward chunk
updateLoadingStates(filteredEvents)
updateLoadingStates(timelineEvents)
return offsetResults.size
}
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
timelineEventEntity = eventEntity,
buildReadReceipts = settings.buildReadReceipts,
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
buildReadReceipts = settings.buildReadReceipts
).let {
// eventually enhance with ui echo?
(uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it)
@ -699,7 +618,7 @@ internal class DefaultTimeline(
private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction,
count: Long): RealmResults<TimelineEventEntity> {
val offsetQuery = filteredEvents.where()
val offsetQuery = timelineEvents.where()
if (direction == Timeline.Direction.BACKWARDS) {
offsetQuery
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
@ -747,7 +666,7 @@ internal class DefaultTimeline(
if (isReady.get().not()) {
return@post
}
updateLoadingStates(filteredEvents)
updateLoadingStates(timelineEvents)
val snapshot = createSnapshot()
val runnable = Runnable {
listeners.forEach {
@ -783,10 +702,10 @@ internal class DefaultTimeline(
return object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> {
TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request")
}
TokenChunkEventPersistor.Result.REACHED_END -> {
TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot()
}
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->

View file

@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
@ -52,7 +51,6 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val paginationTask: PaginationTask,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler
) : TimelineService {
@ -72,7 +70,6 @@ internal class DefaultTimelineService @AssistedInject constructor(
paginationTask = paginationTask,
timelineEventMapper = timelineEventMapper,
settings = settings,
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
timelineInput = timelineInput,
eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,

View file

@ -1,50 +0,0 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.timeline
import io.realm.RealmQuery
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.filterEvents
internal fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(settings: TimelineSettings): RealmQuery<TimelineEventEntity> {
return filterEvents(settings.filters)
}
internal fun List<TimelineEvent>.filterEventsWithSettings(settings: TimelineSettings): List<TimelineEvent> {
return filter { event ->
val filterType = !settings.filters.filterTypes
|| settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) }
if (!filterType) return@filter false
val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) {
val messageContent = event.root.getClearContent().toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else {
true
}
if (!filterEdits) return@filter false
val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted()
!filterRedacted
}
}

View file

@ -1,195 +0,0 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.timeline
import android.util.SparseArray
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.TimelineEventFilter
import org.matrix.android.sdk.internal.database.query.whereInRoom
/**
* This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering).
* When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event.
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
*/
internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val roomId: String,
private val settings: TimelineSettings) {
interface Delegate {
fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean
fun onReadReceiptsUpdated()
}
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private lateinit var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate
private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { collection, changeSet ->
if (!collection.isLoaded || !collection.isValid) {
return@OrderedRealmCollectionChangeListener
}
var hasChange = false
// Deletion here means we don't have any readReceipts for the given hidden events
changeSet.deletions.forEach {
val eventId = correctedReadReceiptsEventByIndex.get(it, "")
val timelineEvent = filteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
.findFirst()
// We are rebuilding the corresponding event with only his own RR
val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)
hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange
}
correctedReadReceiptsEventByIndex.clear()
correctedReadReceiptsByEvent.clear()
for (index in 0 until hiddenReadReceipts.size) {
val summary = hiddenReadReceipts[index] ?: continue
val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue
val isLoaded = nonFilteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null
val displayIndex = timelineEvent.displayIndex
if (isLoaded) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = filteredEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
.findFirst()
// If we find one, we should
if (firstDisplayedEvent != null) {
correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId)
correctedReadReceiptsByEvent
.getOrPut(firstDisplayedEvent.eventId, {
ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts))
})
.addAll(readReceiptsSummaryMapper.map(summary))
}
}
}
if (correctedReadReceiptsByEvent.isNotEmpty()) {
correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) ->
val sortedReadReceipts = correctedReadReceipts.sortedByDescending {
it.originServerTs
}
hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange
}
}
if (hasChange) {
delegate.onReadReceiptsUpdated()
}
}
/**
* Start the realm query subscription. Has to be called on an HandlerThread
*/
fun start(realm: Realm,
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).
this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId)
.isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.`$`)
.isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`)
.filterReceiptsWithSettings()
.findAllAsync()
.also { it.addChangeListener(hiddenReadReceiptsListener) }
}
/**
* Dispose the realm query subscription. Has to be called on an HandlerThread
*/
fun dispose() {
if (this::hiddenReadReceipts.isInitialized) {
this.hiddenReadReceipts.removeAllChangeListeners()
}
}
/**
* Return the current corrected [ReadReceipt] list for an event, or null
*/
fun correctedReadReceipts(eventId: String?): List<ReadReceipt>? {
return correctedReadReceiptsByEvent[eventId]
}
/**
* We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
*/
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
beginGroup()
var needOr = false
if (settings.filters.filterTypes) {
beginGroup()
// Events: A, B, C, D, (E and S1), F, G, (H and S1), I
// Allowed: A, B, C, (E and S1), G, (H and S2)
// Result: D, F, H, I
settings.filters.allowedTypes.forEachIndexed { index, filter ->
if (filter.stateKey == null) {
notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType)
} else {
beginGroup()
notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType)
or()
notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.STATE_KEY}", filter.stateKey)
endGroup()
}
if (index != settings.filters.allowedTypes.size - 1) {
and()
}
}
endGroup()
needOr = true
}
if (settings.filters.filterUseless) {
if (needOr) or()
equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.IS_USELESS}", true)
needOr = true
}
if (settings.filters.filterEdits) {
if (needOr) or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.EDIT)
or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.RESPONSE)
needOr = true
}
if (settings.filters.filterRedacted) {
if (needOr) or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED)
}
endGroup()
return this
}
}

View file

@ -70,15 +70,12 @@ internal class UIEchoManager(
return existingState != sendState
}
// return true if should update
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
var postSnapshot = false
fun onLocalEchoCreated(timelineEvent: TimelineEvent) {
// Manage some ui echos (do it before filter because actual event could be filtered out)
when (timelineEvent.root.getClearType()) {
EventType.REDACTION -> {
}
EventType.REACTION -> {
EventType.REACTION -> {
val content = timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key
@ -91,21 +88,14 @@ internal class UIEchoManager(
reaction = reaction
)
)
postSnapshot = listener.rebuildEvent(relatedEventID) {
listener.rebuildEvent(relatedEventID) {
decorateEventWithReactionUiEcho(it)
} || postSnapshot
}
}
}
}
// do not add events that would have been filtered
if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) {
Timber.v("On local echo created: ${timelineEvent.eventId}")
inMemorySendingEvents.add(0, timelineEvent)
postSnapshot = true
}
return postSnapshot
Timber.v("On local echo created: ${timelineEvent.eventId}")
inMemorySendingEvents.add(0, timelineEvent)
}
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {

View file

@ -339,7 +339,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
// Debug
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.20'

View file

@ -16,6 +16,7 @@
package im.vector.app.core.epoxy
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@ -25,6 +26,17 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute var notBlank: Boolean = false
override fun isVisible() = false
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.updateLayoutParams {
// Force height to 1px so scrolling works correctly
this.height = if (notBlank) 1 else 0
}
}
override fun getEventIds(): List<String> {
return listOf(eventId)

View file

@ -41,7 +41,11 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences:
vectorPreferences.neverShowLongClickOnRoomHelpAgain()
}
fun shouldShowRoomMemberStateEvents(): Boolean {
return vectorPreferences.showRoomMemberStateEvents()
fun shouldShowJoinLeaves(): Boolean {
return vectorPreferences.showJoinLeaveMessages()
}
fun shouldShowAvatarDisplayNameChanges(): Boolean {
return vectorPreferences.showAvatarDisplayNameChangeMessages()
}
}

View file

@ -66,7 +66,7 @@ class JumpToBottomViewVisibilityManager(
}
private fun maybeShowJumpToBottomViewVisibility() {
if (layoutManager.findFirstVisibleItemPosition() != 0) {
if (layoutManager.findFirstVisibleItemPosition() > 1) {
jumpToBottomView.show()
} else {
jumpToBottomView.hide()

View file

@ -1205,7 +1205,6 @@ class RoomDetailFragment @Inject constructor(
if (summary?.membership == Membership.JOIN) {
views.jumpToBottomView.count = summary.notificationCount
views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state)
views.inviteView.visibility = View.GONE
if (state.tombstoneEvent == null) {

View file

@ -1171,16 +1171,15 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
stopTrackingUnreadMessages()
val targetEventId: String = action.eventId
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
if (indexOfEvent == null) {
// Event is not already in RAM
timeline.restartWithEventId(targetEventId)
}
if (action.highlight) {
setState { copy(highlightedEventId = correctedEventId) }
setState { copy(highlightedEventId = targetEventId) }
}
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId))
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId))
}
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
@ -1411,15 +1410,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (events.isEmpty()) return UnreadState.Unknown
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) {
return if (timeline.isLive) {
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
} else {
UnreadState.Unknown
}
}
val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot)
?: return if (timeline.isLive) {
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
} else {
UnreadState.Unknown
}
for (i in (firstDisplayableEventIndex - 1) downTo 0) {
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown

View file

@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import im.vector.app.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
@ -33,8 +32,6 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private val scheduledEventId = AtomicReference<String?>()
var timeline: Timeline? = null
override fun onInserted(position: Int, count: Int) {
scrollIfNeeded()
}
@ -45,9 +42,7 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private fun scrollIfNeeded() {
val eventId = scheduledEventId.get() ?: return
val nonNullTimeline = timeline ?: return
val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId)
val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId)
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()

View file

@ -20,7 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.app.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import timber.log.Timber
import org.matrix.android.sdk.api.extensions.tryOrNull
import java.util.concurrent.CopyOnWriteArrayList
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
@ -38,24 +38,27 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
}
override fun onInserted(position: Int, count: Int) {
if (position != 0) {
return
}
if (forceScroll) {
forceScroll = false
layoutManager.scrollToPosition(position)
layoutManager.scrollToPosition(0)
return
}
Timber.v("On inserted $count count at position: $position")
if (layoutManager.findFirstVisibleItemPosition() != position) {
if (layoutManager.findFirstVisibleItemPosition() > 1) {
return
}
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return
val firstNewItem = tryOrNull {
timelineEventController.adapter.getModelAtPosition(position)
} as? ItemWithEvents ?: return
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position")
repeat(newTimelineEventIds.size - indexOfFirstNewItem) {
newTimelineEventIds.removeAt(indexOfFirstNewItem)
while (newTimelineEventIds.lastOrNull() != firstNewItemIds) {
newTimelineEventIds.removeLastOrNull()
}
layoutManager.scrollToPosition(position)
layoutManager.scrollToPosition(0)
}
}
}

View file

@ -31,17 +31,21 @@ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
@ -49,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer
@ -58,6 +63,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@ -65,8 +71,6 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
@ -77,7 +81,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val session: Session,
private val callManager: WebRtcCallManager,
@TimelineEventControllerHandler
private val backgroundHandler: Handler
private val backgroundHandler: Handler,
private val userPreferencesProvider: UserPreferencesProvider,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
interface Callback :
@ -147,7 +154,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null
private var previousModelsSize = 0
var callback: Callback? = null
var timeline: Timeline? = null
@ -198,7 +204,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker,
adapterPositionMapping,
vectorPreferences,
userPreferencesProvider,
callManager
)
@ -311,7 +317,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} else {
cacheItemData.eventModel
}
listOf(eventModel,
listOf(
cacheItemData?.readReceiptsItem?.takeUnless { mergedHeaderItemFactory.isCollapsed(cacheItemData.localId) },
eventModel,
cacheItemData?.mergedHeaderModel,
cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null }
)
@ -323,61 +331,128 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
hasUTD = false
hasReachedInvite = false
if (modelCache.isEmpty()) {
return
}
val receiptsByEvents = getReadReceiptsByShownEvent()
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
(0 until modelCache.size).forEach { position ->
// 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]?.shouldTriggerBuild() == true) {
modelCache[position] = buildCacheItem(position, currentSnapshot)
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position)
val params = TimelineItemFactoryParams(
event = event,
prevEvent = prevEvent,
nextEvent = nextEvent,
highlightedEventId = eventIdToHighlight,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback
)
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
modelCache[position] = buildCacheItem(params)
}
val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents)
}
}
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition]
val nextEvent = items.nextOrNull(currentPosition)
val prevEvent = items.prevOrNull(currentPosition)
private fun buildCacheItem(params: TimelineItemFactoryParams): CacheItemData {
val event = params.event
if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId, null, null, null)
return CacheItemData(event.localId, event.root.eventId)
}
updateUTDStates(event, nextEvent)
val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also {
updateUTDStates(event, params.nextEvent)
val eventModel = timelineItemFactory.create(params).also {
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
val addDaySeparator = if (hasReachedInvite && hasUTD) {
true
} else {
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
date.toLocalDate() != nextDate?.toLocalDate()
}
val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
return CacheItemData(
localId = event.localId,
eventId = event.root.eventId,
eventModel = eventModel,
shouldTriggerBuild = shouldTriggerBuild)
}
private fun CacheItemData.enrichWithModels(event: TimelineEvent,
nextEvent: TimelineEvent?,
position: Int,
receiptsByEvents: Map<String, List<ReadReceipt>>): CacheItemData {
val wantsDateSeparator = wantsDateSeparator(event, nextEvent)
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent,
items = items,
addDaySeparator = addDaySeparator,
currentPosition = currentPosition,
items = this@TimelineEventController.currentSnapshot,
addDaySeparator = wantsDateSeparator,
currentPosition = position,
eventIdToHighlight = eventIdToHighlight,
callback = callback
) {
requestModelBuild()
}
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
// If we have a SENT decoration, we want to built again as it might have to be changed to NONE if more recent event has also SENT decoration
val forceTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, forceTriggerBuild)
}
private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
return if (addDaySeparator) {
val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
val formattedDayModel = if (wantsDateSeparator) {
buildDaySeparatorItem(event.root.originServerTs)
} else {
null
}
val readReceipts = receiptsByEvents[event.eventId].orEmpty()
return copy(
readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback),
formattedDayModel = formattedDayModel,
mergedHeaderModel = mergedHeaderModel
)
}
private fun searchLastSentEventWithoutReadReceipts(receiptsByEvent: Map<String, List<ReadReceipt>>): String? {
if (timeline?.isLive == false) {
// If timeline is not live we don't want to show SentStatus
return null
}
for (event in currentSnapshot) {
// If there is any RR on the event, we stop searching for Sent event
if (receiptsByEvent[event.eventId]?.isNotEmpty() == true) {
return null
}
// If the event is not shown, we go to the next one
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
continue
}
// If the event is sent by us, we update the holder with the eventId and stop the search
if (event.root.senderId == session.myUserId && event.root.sendState.isSent()) {
return event.eventId
}
}
return null
}
private fun getReadReceiptsByShownEvent(): Map<String, List<ReadReceipt>> {
val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
if (!userPreferencesProvider.shouldShowReadReceipts()) {
return receiptsByEvent
}
var lastShownEventId: String? = null
val itr = currentSnapshot.listIterator(currentSnapshot.size)
while (itr.hasPrevious()) {
val event = itr.previous()
val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId
}
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
lastShownEventId = event.eventId
}
if (lastShownEventId == null) {
continue
}
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
existingReceipts.addAll(currentReadReceipts)
}
return receiptsByEvent
}
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
}
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
@ -409,6 +484,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
return if (hasReachedInvite && hasUTD) {
true
} else {
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
date.toLocalDate() != nextDate?.toLocalDate()
}
}
/**
* Return true if added
*/
@ -429,14 +514,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private data class CacheItemData(
val localId: Long,
val eventId: String?,
val readReceiptsItem: ReadReceiptsItem? = null,
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null,
val forceTriggerBuild: Boolean = false
) {
fun shouldTriggerBuild(): Boolean {
// Since those items can change when we paginate, force a re-build
return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
}
}
val shouldTriggerBuild: Boolean = false
)
}

View file

@ -46,13 +46,11 @@ class CallItemFactory @Inject constructor(
private val callManager: WebRtcCallManager
) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
if (event.root.eventId == null) return null
val roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, null, null)
val informationData = messageInformationDataFactory.create(params)
val callSignalingContent = event.getCallSignallingContent() ?: return null
val callId = callSignalingContent.callId ?: return null
val call = callManager.getCallById(callId)
@ -68,8 +66,8 @@ class CallItemFactory @Inject constructor(
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
callKind = callKind,
callback = callback,
highlight = highlight,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = call != null
)
@ -80,8 +78,8 @@ class CallItemFactory @Inject constructor(
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.INVITED,
callKind = callKind,
callback = callback,
highlight = highlight,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = call != null
)
@ -92,8 +90,8 @@ class CallItemFactory @Inject constructor(
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.REJECTED,
callKind = callKind,
callback = callback,
highlight = highlight,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = false
)
@ -104,8 +102,8 @@ class CallItemFactory @Inject constructor(
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.ENDED,
callKind = callKind,
callback = callback,
highlight = highlight,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = false
)

View file

@ -25,7 +25,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio
import im.vector.app.features.home.room.detail.timeline.item.DefaultItem
import im.vector.app.features.home.room.detail.timeline.item.DefaultItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider,
@ -43,8 +42,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
text = text,
itemLongClickListener = { view ->
callback?.onEventLongClicked(informationData, null, view) ?: false
},
readReceiptsCallback = callback
}
)
return DefaultItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
@ -52,16 +50,14 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
.attributes(attributes)
}
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?,
throwable: Throwable? = null): DefaultItem {
fun create(params: TimelineItemFactoryParams, throwable: Throwable? = null): DefaultItem {
val event = params.event
val text = if (throwable == null) {
stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType())
} else {
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
}
val informationData = informationDataFactory.create(event, null, null)
return create(text, informationData, highlight, callback)
val informationData = informationDataFactory.create(params)
return create(text, informationData, params.isHighlighted, params.callback)
}
}

View file

@ -21,7 +21,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
@ -33,7 +32,6 @@ import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import javax.inject.Inject
@ -46,11 +44,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val attributesFactory: MessageItemAttributesFactory,
private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
event.root.eventId ?: return null
return when {
@ -109,14 +104,14 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
}
}
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
val informationData = messageInformationDataFactory.create(params)
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, params.callback)
return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.highlighted(params.isHighlighted)
.attributes(attributes)
.message(spannableStr)
.movementMethod(createLinkMovementMethod(callback))
.movementMethod(createLinkMovementMethod(params.callback))
}
else -> null
}

View file

@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
@ -28,7 +27,6 @@ import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineI
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import javax.inject.Inject
@ -41,15 +39,14 @@ class EncryptionItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): StatusTileTimelineItem? {
fun create(params: TimelineItemFactoryParams): StatusTileTimelineItem? {
val event = params.event
if (!event.root.isStateEvent()) {
return null
}
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(event, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
val title: String
@ -86,7 +83,7 @@ class EncryptionItemFactory @Inject constructor(
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}

View file

@ -23,9 +23,9 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
@ -47,7 +47,8 @@ import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider,
private val roomSummariesHolder: RoomSummariesHolder) {
private val roomSummariesHolder: RoomSummariesHolder,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@ -85,12 +86,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
return if (prevSameTypeEvents.isEmpty()) {
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight)
return if (mergedEvents.isEmpty()) {
null
} else {
var highlighted = false
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
@ -126,8 +126,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
},
readReceiptsCallback = callback
}
)
MergedMembershipEventsItem_()
.id(mergeId)
@ -205,7 +204,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
},
hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback,
callback = callback,
currentUserId = currentUserId,
roomSummary = roomSummariesHolder.get(event.roomId),

View file

@ -85,7 +85,6 @@ import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
@ -118,15 +117,13 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId)
}
fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val highlight = params.isHighlighted
val callback = params.callback
event.root.eventId ?: return null
roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
val informationData = messageInformationDataFactory.create(params)
if (event.root.isRedacted()) {
// message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
@ -142,7 +139,7 @@ class MessageItemFactory @Inject constructor(
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) {
// This is an edit event, we should display it when debugging as a notice event
return noticeItemFactory.create(event, highlight, callback)
return noticeItemFactory.create(params)
}
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
@ -158,7 +155,7 @@ class MessageItemFactory @Inject constructor(
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
is MessagePollResponseContent -> noticeItemFactory.create(params)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
}

View file

@ -17,13 +17,11 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
@ -31,24 +29,23 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
private val informationDataFactory: MessageInformationDataFactory,
private val avatarSizeProvider: AvatarSizeProvider) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
fun create(params: TimelineItemFactoryParams): NoticeItem? {
val event = params.event
val formattedText = eventFormatter.format(event) ?: return null
val informationData = informationDataFactory.create(event, null, null)
val informationData = informationDataFactory.create(params)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,
informationData = informationData,
noticeText = formattedText,
itemLongClickListener = { view ->
callback?.onEventLongClicked(informationData, null, view) ?: false
params.callback?.onEventLongClicked(informationData, null, view) ?: false
},
readReceiptsCallback = callback,
avatarClickListener = { callback?.onAvatarClicked(informationData) }
readReceiptsCallback = params.callback,
avatarClickListener = { params.callback?.onAvatarClicked(informationData) }
)
return NoticeItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.highlighted(params.isHighlighted)
.attributes(attributes)
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.factory
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import javax.inject.Inject
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
fun create(eventId: String, readReceipts: List<ReadReceipt>, callback: TimelineEventController.Callback?): ReadReceiptsItem? {
if (readReceipts.isEmpty()) {
return null
}
val readReceiptsData = readReceipts
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
.toList()
return ReadReceiptsItem_()
.id("read_receipts_$eventId")
.eventId(eventId)
.readReceipts(readReceiptsData)
.avatarRenderer(avatarRenderer)
.clickListener(DebouncedClickListener({ _ ->
callback?.onReadReceiptsClicked(readReceiptsData)
}))
}
}

View file

@ -20,13 +20,11 @@ import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider,
@ -34,25 +32,26 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri
private val session: Session,
private val noticeItemFactory: NoticeItemFactory) {
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>() ?: return null
val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(event, callback)
val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params)
val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null
val text = span {
+stringProvider.getString(R.string.room_tombstone_continuation_description)
+"\n"
span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) {
textDecorationLine = "underline"
onClick = { callback?.onRoomCreateLinkClicked(roomLink) }
onClick = { params.callback?.onRoomCreateLinkClicked(roomLink) }
}
}
return RoomCreateItem_()
.text(text)
}
private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
private fun defaultRendering(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
noticeItemFactory.create(event, false, callback)
noticeItemFactory.create(params)
} else {
null
}

View file

@ -19,8 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber
@ -35,23 +34,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val widgetItemFactory: WidgetItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory,
private val callItemFactory: CallItemFactory,
private val userPreferencesProvider: UserPreferencesProvider) {
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
/**
* Reminder: nextEvent is older and prevEvent is newer.
*/
fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> {
val event = params.event
val computedModel = try {
if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) {
return buildEmptyItem(event, params.prevEvent, params.highlightedEventId)
}
when (event.root.getClearType()) {
// Message itemsX
EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
// State and call
EventType.MESSAGE -> messageItemFactory.create(params)
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
@ -63,68 +60,61 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback)
// Crypto
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
} else {
encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
}
}
EventType.REDACTION,
EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.REACTION,
EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE -> {
// TODO These are not filtered out by timeline when encrypted
// For now manually ignore
if (userPreferencesProvider.shouldShowHiddenEvents()) {
noticeItemFactory.create(event, highlight, callback)
EventType.CALL_NEGOTIATE,
EventType.REACTION,
EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params)
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params)
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(params)
} else {
null
encryptedItemFactory.create(params)
}
}
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(event, highlight, callback)
verificationConclusionItemFactory.create(params)
}
// Unhandled event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(event, highlight, callback)
defaultItemFactory.create(params)
}
}
} catch (throwable: Throwable) {
Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(event, highlight, callback, throwable)
defaultItemFactory.create(params, throwable)
}
return computedModel ?: buildEmptyItem(event)
return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId)
}
private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem {
private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?): TimelineEmptyItem {
val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId)
return TimelineEmptyItem_()
.id(timelineEvent.localId)
.eventId(timelineEvent.eventId)
.notBlank(isNotBlank)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class TimelineItemFactoryParams(
val event: TimelineEvent,
val prevEvent: TimelineEvent? = null,
val nextEvent: TimelineEvent? = null,
val highlightedEventId: String? = null,
val lastSentEventIdWithoutReadReceipts: String? = null,
val callback: TimelineEventController.Callback? = null
) {
val isHighlighted = highlightedEventId == event.eventId
}

View file

@ -20,7 +20,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
/**
@ -54,37 +52,35 @@ class VerificationItemFactory @Inject constructor(
private val session: Session
) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
if (event.root.eventId == null) return null
val relContent: MessageRelationContent = event.root.content.toModel()
?: event.root.getClearContent().toModel()
?: return ignoredConclusion(event, highlight, callback)
?: return ignoredConclusion(params)
if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback)
if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(params)
val refEventId = relContent.relatesTo?.eventId
?: return ignoredConclusion(event, highlight, callback)
?: return ignoredConclusion(params)
// If we cannot find the referenced request we do not display the done event
val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId)
?: return ignoredConclusion(event, highlight, callback)
?: return ignoredConclusion(params)
// If it's not a request ignore this event
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
val referenceInformationData = messageInformationDataFactory.create(refEvent, null, null)
val referenceInformationData = messageInformationDataFactory.create(TimelineItemFactoryParams(refEvent))
val informationData = messageInformationDataFactory.create(event, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val informationData = messageInformationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
when (event.root.getClearType()) {
EventType.KEY_VERIFICATION_CANCEL -> {
// Is the request referenced is actually really cancelled?
val cancelContent = event.root.getClearContent().toModel<MessageVerificationCancelContent>()
?: return ignoredConclusion(event, highlight, callback)
?: return ignoredConclusion(params)
when (safeValueOf(cancelContent.code)) {
CancelCode.MismatchedCommitment,
@ -107,22 +103,22 @@ class VerificationItemFactory @Inject constructor(
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
else -> return ignoredConclusion(event, highlight, callback)
else -> return ignoredConclusion(params)
}
}
EventType.KEY_VERIFICATION_DONE -> {
// Is the request referenced is actually really completed?
if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) {
return ignoredConclusion(event, highlight, callback)
return ignoredConclusion(params)
}
// We only tale the one sent by me
if (informationData.sentByMe) {
// We only display the done sent by the other user, the done send by me is ignored
return ignoredConclusion(event, highlight, callback)
return ignoredConclusion(params)
}
return StatusTileTimelineItem_()
.attributes(
@ -140,18 +136,15 @@ class VerificationItemFactory @Inject constructor(
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}
return null
}
private fun ignoredConclusion(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
private fun ignoredConclusion(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(params)
return null
}
}

View file

@ -20,7 +20,6 @@ import im.vector.app.ActiveSessionDataSource
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
@ -29,7 +28,6 @@ import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineI
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import javax.inject.Inject
@ -47,25 +45,24 @@ class WidgetItemFactory @Inject constructor(
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) {
WidgetType.Jitsi -> createJitsiItem(event, callback, widgetContent, previousWidgetContent)
WidgetType.Jitsi -> createJitsiItem(params, widgetContent, previousWidgetContent)
// There is lot of other widget types we could improve here
else -> noticeItemFactory.create(event, highlight, callback)
else -> noticeItemFactory.create(params)
}
}
private fun createJitsiItem(timelineEvent: TimelineEvent,
callback: TimelineEventController.Callback?,
private fun createJitsiItem(params: TimelineItemFactoryParams,
widgetContent: WidgetContent,
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
val informationData = informationDataFactory.create(timelineEvent, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val timelineEvent = params.event
val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
val message = if (widgetContent.isActive()) {

View file

@ -19,11 +19,11 @@ package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.settings.VectorPreferences
@ -51,9 +51,10 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData {
// Non nullability has been tested before
val eventId = event.root.eventId!!
fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event
val nextEvent = params.nextEvent
val eventId = event.eventId
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
@ -76,9 +77,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val isSentByMe = event.root.senderId == session.myUserId
val sendStateDecoration = if (isSentByMe) {
getSendStateDecoration(
eventSendState = event.root.sendState,
prevEventSendState = prevEvent?.root?.sendState,
anyReadReceipts = event.readReceipts.any { it.user.userId != session.myUserId },
event = event,
lastSentEventWithoutReadReceipts = params.lastSentEventIdWithoutReadReceipts,
isMedia = event.root.isAttachmentMessage()
)
} else {
@ -111,15 +111,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
},
hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
readReceipts = event.readReceipts
.asSequence()
.filter {
it.user.userId != session.myUserId
}
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
.toList(),
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState
?: VerificationState.REQUEST
@ -131,15 +122,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
)
}
private fun getSendStateDecoration(eventSendState: SendState,
prevEventSendState: SendState?,
anyReadReceipts: Boolean,
private fun getSendStateDecoration(event: TimelineEvent,
lastSentEventWithoutReadReceipts: String?,
isMedia: Boolean): SendStateDecoration {
val eventSendState = event.root.sendState
return if (eventSendState.isSending()) {
if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA
} else if (eventSendState.hasFailed()) {
SendStateDecoration.FAILED
} else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) {
} else if (lastSentEventWithoutReadReceipts == event.eventId) {
SendStateDecoration.SENT
} else {
SendStateDecoration.NONE

View file

@ -20,13 +20,14 @@ import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import kotlin.reflect.KMutableProperty0
@ -34,7 +35,7 @@ private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
private val adapterPositionMapping: MutableMap<String, Int>,
private val vectorPreferences: VectorPreferences,
private val userPreferencesProvider: UserPreferencesProvider,
private val callManager: WebRtcCallManager
) {
@ -56,23 +57,39 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
var atLeastOneVisibleItemSinceLastDaySeparator = false
var atLeastOneVisibleItemsBeforeReadMarker = false
// Then iterate on models so we have the exact positions in the adapter
modelsIterator.forEach { epoxyModel ->
if (epoxyModel is ItemWithEvents) {
if (epoxyModel.isVisible()) {
atLeastOneVisibleItemSinceLastDaySeparator = true
atLeastOneVisibleItemsBeforeReadMarker = true
}
epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index
if (eventId == firstUnreadEventId) {
if (epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) {
modelsIterator.addReadMarkerItem(callback)
index++
positionOfReadMarker.set(index)
}
}
}
if (epoxyModel is CallTileTimelineItem) {
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
if (epoxyModel is DaySeparatorItem) {
if (!atLeastOneVisibleItemSinceLastDaySeparator) {
modelsIterator.remove()
return@forEach
}
atLeastOneVisibleItemSinceLastDaySeparator = false
} else if (epoxyModel is CallTileTimelineItem) {
val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
if (!hasBeenRemoved) {
atLeastOneVisibleItemSinceLastDaySeparator = true
}
}
index++
}
@ -94,20 +111,23 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
epoxyModel: CallTileTimelineItem,
callIds: MutableSet<String>,
showHiddenEvents: Boolean
) {
): Boolean {
val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or
// if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) {
val removed = shouldRemoveCallItem && !showHiddenEvents
if (removed) {
remove()
val emptyItem = TimelineEmptyItem_()
.id(epoxyModel.id())
.eventId(epoxyModel.attributes.informationData.eventId)
.notBlank(false)
add(emptyItem)
}
callIds.add(callId)
return removed
}
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {

View file

@ -16,12 +16,14 @@
package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.extensions.localDateTime
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
object TimelineDisplayableEvents {
/**
* All types we have an item to build with. Every type not defined here will be shown as DefaultItem if forced to be shown, otherwise will be hidden.
*/
val DISPLAYABLE_TYPES = listOf(
EventType.MESSAGE,
EventType.STATE_ROOM_WIDGET_LEGACY,
@ -68,7 +70,7 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.STATE_ROOM_ENCRYPTION -> true
EventType.STATE_ROOM_MEMBER -> {
EventType.STATE_ROOM_MEMBER -> {
// Keep only room member events regarding the room creator (when he joined the room),
// but exclude events where the room creator invite others, or where others join
roomCreatorUserId != null && root.stateKey == roomCreatorUserId
@ -76,39 +78,3 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
else -> false
}
}
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
if (index >= size - 1) {
return emptyList()
}
val timelineEvent = this[index]
val nextSubList = subList(index + 1, size)
val indexOfNextDay = nextSubList.indexOfFirst {
val date = it.root.localDateTime()
val nextDate = timelineEvent.root.localDateTime()
date.toLocalDate() != nextDate.toLocalDate()
}
val nextSameDayEvents = if (indexOfNextDay == -1) {
nextSubList
} else {
nextSubList.subList(0, indexOfNextDay)
}
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() }
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
nextSameDayEvents
} else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
}
if (sameTypeEvents.size < minSize) {
return emptyList()
}
return sameTypeEvents
}
fun List<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
val prevSub = subList(0, index + 1)
return prevSub
.reversed()
.nextSameTypeEvents(0, minSize)
.reversed()
}

View file

@ -0,0 +1,152 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.helper
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.UserPreferencesProvider
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
/**
* @param timelineEvents the events to search in
* @param index the index to start computing (inclusive)
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
* @param eventIdToHighlight used to compute visibility
*
* @return a list of timeline events which have sequentially the same type following the next direction.
*/
fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?): List<TimelineEvent> {
if (index >= timelineEvents.size - 1) {
return emptyList()
}
val timelineEvent = timelineEvents[index]
val nextSubList = timelineEvents.subList(index, timelineEvents.size)
val indexOfNextDay = nextSubList.indexOfFirst {
val date = it.root.localDateTime()
val nextDate = timelineEvent.root.localDateTime()
date.toLocalDate() != nextDate.toLocalDate()
}
val nextSameDayEvents = if (indexOfNextDay == -1) {
nextSubList
} else {
nextSubList.subList(0, indexOfNextDay)
}
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() }
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
nextSameDayEvents
} else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
}
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight) }
if (filteredSameTypeEvents.size < minSize) {
return emptyList()
}
return filteredSameTypeEvents
}
/**
* @param timelineEvents the events to search in
* @param index the index to start computing (inclusive)
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
* @param eventIdToHighlight used to compute visibility
*
* @return a list of timeline events which have sequentially the same type following the prev direction.
*/
fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?): List<TimelineEvent> {
val prevSub = timelineEvents.subList(0, index + 1)
return prevSub
.reversed()
.let {
nextSameTypeEvents(it, 0, minSize, eventIdToHighlight)
}
}
/**
* @param timelineEvent the event to check for visibility
* @param highlightedEventId can be checked to force visibility to true
* @return true if the event should be shown in the timeline.
*/
fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?): Boolean {
// If show hidden events is true we should always display something
if (userPreferencesProvider.shouldShowHiddenEvents()) {
return true
}
// We always show highlighted event
if (timelineEvent.eventId == highlightedEventId) {
return true
}
if (!timelineEvent.isDisplayable()) {
return false
}
// Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences.
return !timelineEvent.shouldBeHidden()
}
private fun TimelineEvent.isDisplayable(): Boolean {
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType())
}
private fun TimelineEvent.shouldBeHidden(): Boolean {
if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) {
return true
}
if (root.getRelationContent()?.type == RelationType.REPLACE) {
return true
}
if (root.getClearType() == EventType.STATE_ROOM_MEMBER) {
val diff = computeMembershipDiff()
if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true
if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true
}
return false
}
private fun TimelineEvent.computeMembershipDiff(): MembershipDiff {
val content = root.getClearContent().toModel<RoomMemberContent>()
val prevContent = root.resolvedPrevContent().toModel<RoomMemberContent>()
val isMembershipChanged = content?.membership != prevContent?.membership
val isJoin = isMembershipChanged && content?.membership == Membership.JOIN
val isPart = isMembershipChanged && content?.membership == Membership.LEAVE && root.stateKey == root.senderId
val isProfileChanged = !isMembershipChanged && content?.membership == Membership.JOIN
val isDisplaynameChange = isProfileChanged && content?.displayName != prevContent?.displayName
val isAvatarChange = isProfileChanged && content?.avatarUrl !== prevContent?.avatarUrl
return MembershipDiff(
isJoin = isJoin,
isPart = isPart,
isDisplaynameChange = isDisplaynameChange,
isAvatarChange = isAvatarChange
)
}
private data class MembershipDiff(
val isJoin: Boolean,
val isPart: Boolean,
val isDisplaynameChange: Boolean,
val isAvatarChange: Boolean
)
}

View file

@ -17,48 +17,14 @@
package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.resources.UserPreferencesProvider
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import javax.inject.Inject
class TimelineSettingsFactory @Inject constructor(
private val userPreferencesProvider: UserPreferencesProvider,
private val session: Session
) {
class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
fun create(): TimelineSettings {
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineSettings(
initialSize = 30,
filters = TimelineEventFilters(
filterEdits = false,
filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(),
filterUseless = false,
filterTypes = false),
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
} else {
val allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES.createAllowedEventTypeFilters()
TimelineSettings(
initialSize = 30,
filters = TimelineEventFilters(
filterEdits = true,
filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(),
filterUseless = true,
filterTypes = true,
allowedTypes = allowedTypes),
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
}
}
private fun List<String>.createAllowedEventTypeFilters(): List<EventTypeFilter> {
return map {
EventTypeFilter(
eventType = it,
stateKey = if (it == EventType.STATE_ROOM_MEMBER && !userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null
)
}
return TimelineSettings(
initialSize = 30,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
}
}

View file

@ -24,7 +24,6 @@ import androidx.annotation.IdRes
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@ -41,10 +40,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
abstract val baseAttributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
})
private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) {
baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
@ -69,12 +64,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
override fun bind(holder: H) {
super.bind(holder)
holder.readReceiptsView.render(
baseAttributes.informationData.readReceipts,
baseAttributes.avatarRenderer,
_readReceiptsClickListener
)
val reactions = baseAttributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false
@ -111,7 +100,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
override fun unbind(holder: H) {
holder.reactionsContainer.setOnLongClickListener(null)
holder.readReceiptsView.unbind(baseAttributes.avatarRenderer)
super.unbind(holder)
}

View file

@ -26,7 +26,6 @@ import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.platform.CheckableView
import im.vector.app.core.ui.views.ReadReceiptsView
import im.vector.app.core.utils.DimensionConverter
/**
@ -56,7 +55,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<View>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
override fun bindView(itemView: View) {
super.bindView(itemView)

View file

@ -19,10 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.view.View
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.util.MatrixItem
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
@ -41,8 +39,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse)
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
protected val distinctMergeData by lazy {
@ -72,7 +68,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
val isCollapsed: Boolean
val mergeData: List<Data>
val avatarRenderer: AvatarRenderer
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
val onCollapsedStateChanged: (Boolean) -> Unit
}

View file

@ -22,9 +22,7 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
@ -32,21 +30,15 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
@EpoxyAttribute
lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
override fun bind(holder: Holder) {
super.bind(holder)
holder.messageTextView.text = attributes.text
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
}
override fun unbind(holder: Holder) {
attributes.avatarRenderer.clear(holder.avatarImageView)
holder.readReceiptsView.unbind(attributes.avatarRenderer)
super.unbind(holder)
}
@ -65,8 +57,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
val avatarRenderer: AvatarRenderer,
val informationData: MessageInformationData,
val text: CharSequence,
val itemLongClickListener: View.OnLongClickListener? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
val itemLongClickListener: View.OnLongClickListener? = null
)
companion object {

View file

@ -22,4 +22,8 @@ interface ItemWithEvents {
* Will generally get only one, but it handles the merged items.
*/
fun getEventIds(): List<String>
fun canAppendReadMarker(): Boolean = true
fun isVisible(): Boolean = true
}

View file

@ -21,12 +21,10 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.children
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
@ -56,8 +54,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
holder.avatarListView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
class Holder : BasedMergedItem.Holder(STUB_ID) {
@ -73,7 +69,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit
) : BasedMergedItem.Attributes
}

View file

@ -92,8 +92,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.summaryView.visibility = View.GONE
holder.encryptionTile.isGone = true
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
private fun bindEncryptionTile(holder: Holder, data: Data?) {
@ -223,7 +221,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit,
val callback: TimelineEventController.Callback? = null,
val currentUserId: String,

View file

@ -36,10 +36,8 @@ data class MessageInformationData(
/*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null,
val pollResponseAggregatedSummary: PollResponseData? = null,
val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null,
val sentByMe: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,

View file

@ -25,7 +25,6 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
@ -36,16 +35,11 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
@EpoxyAttribute
lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
override fun bind(holder: Holder) {
super.bind(holder)
holder.noticeTextView.text = attributes.noticeText
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
holder.avatarImageView.onClick(attributes.avatarClickListener)
when (attributes.informationData.e2eDecoration) {
@ -62,7 +56,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
override fun unbind(holder: Holder) {
attributes.avatarRenderer.clear(holder.avatarImageView)
holder.readReceiptsView.unbind(attributes.avatarRenderer)
super.unbind(holder)
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.item
import android.view.View
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.ui.views.ReadReceiptsView
import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts)
abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: View.OnClickListener
override fun canAppendReadMarker(): Boolean = false
override fun getEventIds(): List<String> = listOf(eventId)
override fun bind(holder: Holder) {
super.bind(holder)
holder.readReceiptsView.render(readReceipts, avatarRenderer, clickListener)
}
override fun unbind(holder: Holder) {
holder.readReceiptsView.unbind(avatarRenderer)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
}
}

View file

@ -357,15 +357,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false)
}
/**
* Tells if all room member state events should be shown in the messages list.
*
* @return true all room member state events should be shown in the messages list.
*/
fun showRoomMemberStateEvents(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SHOW_ROOM_MEMBER_STATE_EVENTS_KEY, true)
}
/**
* Tells if the join and leave membership events should be shown in the messages list.
*

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp" />
android:layout_height="0dp" />

View file

@ -188,15 +188,6 @@
android:layout_height="wrap_content" /-->
</com.google.android.flexbox.FlexboxLayout>
<im.vector.app.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" />
</LinearLayout>
</RelativeLayout>

View file

@ -10,7 +10,7 @@
android:id="@+id/messageSelectedBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="@+id/readReceiptsView"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true"
android:background="@drawable/highlighted_message_background" />
@ -80,14 +80,4 @@
android:visibility="gone"
tools:visibility="visible" />
<im.vector.app.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/viewStubContainer"
android:layout_alignParentEnd="true"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</RelativeLayout>

View file

@ -120,14 +120,6 @@
</com.google.android.flexbox.FlexboxLayout>
<im.vector.app.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" />
</LinearLayout>
</RelativeLayout>

View file

@ -0,0 +1,14 @@
<?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">
<im.vector.app.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" />
</FrameLayout>

View file

@ -88,9 +88,15 @@
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_SHOW_ROOM_MEMBER_STATE_EVENTS_KEY"
android:summary="@string/settings_show_room_member_state_events_summary"
android:title="@string/settings_show_room_member_state_events" />
android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"
android:summary="@string/settings_show_join_leave_messages_summary"
android:title="@string/settings_show_join_leave_messages" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY"
android:summary="@string/settings_show_avatar_display_name_changes_messages_summary"
android:title="@string/settings_show_avatar_display_name_changes_messages" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"