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) - Api interceptor to allow app developers peek responses (#2986)
- Update reactions to Unicode 13.1 (#2998) - Update reactions to Unicode 13.1 (#2998)
- Be more robust when parsing some enums - 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 🐛: Bugfix 🐛:
- Fix bad theme change for the MainActivity - Fix bad theme change for the MainActivity
@ -24,6 +25,7 @@ Translations 🗣:
SDK API changes ⚠️: SDK API changes ⚠️:
- Several Services have been migrated to coroutines (#2449) - Several Services have been migrated to coroutines (#2449)
- Removes filtering options on Timeline.
Build 🧱: Build 🧱:
- -

View File

@ -95,12 +95,6 @@ interface Timeline {
*/ */
fun getTimelineEventWithId(eventId: String?): TimelineEvent? 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 { interface Listener {
/** /**
* Call when the timeline has been updated through pagination or sync. * 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. * 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, val initialSize: Int,
/**
* Filters for timeline event
*/
val filters: TimelineEventFilters = TimelineEventFilters(),
/** /**
* If true, will build read receipts for each event. * If true, will build read receipts for each event.
*/ */

View File

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

View File

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

View File

@ -16,6 +16,7 @@
package im.vector.app.core.epoxy package im.vector.app.core.epoxy
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R 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 { abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String @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> { override fun getEventIds(): List<String> {
return listOf(eventId) return listOf(eventId)

View File

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

View File

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

View File

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

View File

@ -1171,16 +1171,15 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
stopTrackingUnreadMessages() stopTrackingUnreadMessages()
val targetEventId: String = action.eventId val targetEventId: String = action.eventId
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
if (indexOfEvent == null) { if (indexOfEvent == null) {
// Event is not already in RAM // Event is not already in RAM
timeline.restartWithEventId(targetEventId) timeline.restartWithEventId(targetEventId)
} }
if (action.highlight) { 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) { private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
@ -1411,15 +1410,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState { private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (events.isEmpty()) return UnreadState.Unknown if (events.isEmpty()) return UnreadState.Unknown
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot)
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) ?: return if (timeline.isLive) {
if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) { UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
return if (timeline.isLive) { } else {
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) UnreadState.Unknown
} else { }
UnreadState.Unknown
}
}
for (i in (firstDisplayableEventIndex - 1) downTo 0) { for (i in (firstDisplayableEventIndex - 1) downTo 0) {
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
val eventId = timelineEvent.root.eventId ?: 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 androidx.recyclerview.widget.RecyclerView
import im.vector.app.core.platform.DefaultListUpdateCallback 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.TimelineEventController
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -33,8 +32,6 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private val scheduledEventId = AtomicReference<String?>() private val scheduledEventId = AtomicReference<String?>()
var timeline: Timeline? = null
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
scrollIfNeeded() scrollIfNeeded()
} }
@ -45,9 +42,7 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private fun scrollIfNeeded() { private fun scrollIfNeeded() {
val eventId = scheduledEventId.get() ?: return val eventId = scheduledEventId.get() ?: return
val nonNullTimeline = timeline ?: return val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId)
val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId)
if (positionToScroll != null) { if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() 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.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents 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 import java.util.concurrent.CopyOnWriteArrayList
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
@ -38,24 +38,27 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
} }
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
if (position != 0) {
return
}
if (forceScroll) { if (forceScroll) {
forceScroll = false forceScroll = false
layoutManager.scrollToPosition(position) layoutManager.scrollToPosition(0)
return return
} }
Timber.v("On inserted $count count at position: $position") if (layoutManager.findFirstVisibleItemPosition() > 1) {
if (layoutManager.findFirstVisibleItemPosition() != position) {
return 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 firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) { if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position") while (newTimelineEventIds.lastOrNull() != firstNewItemIds) {
repeat(newTimelineEventIds.size - indexOfFirstNewItem) { newTimelineEventIds.removeLastOrNull()
newTimelineEventIds.removeAt(indexOfFirstNewItem)
} }
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.localDateTime
import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.RoomDetailAction 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.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState 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.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.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder 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.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper 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.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.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.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem 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.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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.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.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer 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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent 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 org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
@ -77,7 +81,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val session: Session, private val session: Session,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
@TimelineEventControllerHandler @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 { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
interface Callback : interface Callback :
@ -147,7 +154,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private var unreadState: UnreadState = UnreadState.Unknown private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null private var eventIdToHighlight: String? = null
private var previousModelsSize = 0
var callback: Callback? = null var callback: Callback? = null
var timeline: Timeline? = null var timeline: Timeline? = null
@ -198,7 +204,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val interceptorHelper = TimelineControllerInterceptorHelper( private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker, ::positionOfReadMarker,
adapterPositionMapping, adapterPositionMapping,
vectorPreferences, userPreferencesProvider,
callManager callManager
) )
@ -311,7 +317,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} else { } else {
cacheItemData.eventModel cacheItemData.eventModel
} }
listOf(eventModel, listOf(
cacheItemData?.readReceiptsItem?.takeUnless { mergedHeaderItemFactory.isCollapsed(cacheItemData.localId) },
eventModel,
cacheItemData?.mergedHeaderModel, cacheItemData?.mergedHeaderModel,
cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null } 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) { private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
hasUTD = false hasUTD = false
hasReachedInvite = false hasReachedInvite = false
if (modelCache.isEmpty()) { if (modelCache.isEmpty()) {
return return
} }
val receiptsByEvents = getReadReceiptsByShownEvent()
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
(0 until modelCache.size).forEach { position -> (0 until modelCache.size).forEach { position ->
// Should be build if not cached or if cached but contains additional models val event = currentSnapshot[position]
// We then are sure we always have items up to date. val nextEvent = currentSnapshot.nextOrNull(position)
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { val prevEvent = currentSnapshot.prevOrNull(position)
modelCache[position] = buildCacheItem(position, currentSnapshot) 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 { private fun buildCacheItem(params: TimelineItemFactoryParams): CacheItemData {
val event = items[currentPosition] val event = params.event
val nextEvent = items.nextOrNull(currentPosition)
val prevEvent = items.prevOrNull(currentPosition)
if (hasReachedInvite && hasUTD) { if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId, null, null, null) return CacheItemData(event.localId, event.root.eventId)
} }
updateUTDStates(event, nextEvent) updateUTDStates(event, params.nextEvent)
val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also { val eventModel = timelineItemFactory.create(params).also {
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
} }
val addDaySeparator = if (hasReachedInvite && hasUTD) { val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
true return CacheItemData(
} else { localId = event.localId,
val date = event.root.localDateTime() eventId = event.root.eventId,
val nextDate = nextEvent?.root?.localDateTime() eventModel = eventModel,
date.toLocalDate() != nextDate?.toLocalDate() 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, val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent, nextEvent = nextEvent,
items = items, items = this@TimelineEventController.currentSnapshot,
addDaySeparator = addDaySeparator, addDaySeparator = wantsDateSeparator,
currentPosition = currentPosition, currentPosition = position,
eventIdToHighlight = eventIdToHighlight, eventIdToHighlight = eventIdToHighlight,
callback = callback callback = callback
) { ) {
requestModelBuild() requestModelBuild()
} }
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs) val formattedDayModel = if (wantsDateSeparator) {
// 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 buildDaySeparatorItem(event.root.originServerTs)
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)
} else { } else {
null 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_ { 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 * Return true if added
*/ */
@ -429,14 +514,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private data class CacheItemData( private data class CacheItemData(
val localId: Long, val localId: Long,
val eventId: String?, val eventId: String?,
val readReceiptsItem: ReadReceiptsItem? = null,
val eventModel: EpoxyModel<*>? = null, val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null, val formattedDayModel: DaySeparatorItem? = null,
val forceTriggerBuild: Boolean = false val shouldTriggerBuild: Boolean = false
) { )
fun shouldTriggerBuild(): Boolean {
// Since those items can change when we paginate, force a re-build
return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
}
}
} }

View File

@ -46,13 +46,11 @@ class CallItemFactory @Inject constructor(
private val callManager: WebRtcCallManager private val callManager: WebRtcCallManager
) { ) {
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
highlight: Boolean, val event = params.event
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (event.root.eventId == null) return null if (event.root.eventId == null) return null
val roomId = event.roomId val roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, null, null) val informationData = messageInformationDataFactory.create(params)
val callSignalingContent = event.getCallSignallingContent() ?: return null val callSignalingContent = event.getCallSignallingContent() ?: return null
val callId = callSignalingContent.callId ?: return null val callId = callSignalingContent.callId ?: return null
val call = callManager.getCallById(callId) val call = callManager.getCallById(callId)
@ -68,8 +66,8 @@ class CallItemFactory @Inject constructor(
callId = callId, callId = callId,
callStatus = CallTileTimelineItem.CallStatus.IN_CALL, callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
callKind = callKind, callKind = callKind,
callback = callback, callback = params.callback,
highlight = highlight, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = call != null isStillActive = call != null
) )
@ -80,8 +78,8 @@ class CallItemFactory @Inject constructor(
callId = callId, callId = callId,
callStatus = CallTileTimelineItem.CallStatus.INVITED, callStatus = CallTileTimelineItem.CallStatus.INVITED,
callKind = callKind, callKind = callKind,
callback = callback, callback = params.callback,
highlight = highlight, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = call != null isStillActive = call != null
) )
@ -92,8 +90,8 @@ class CallItemFactory @Inject constructor(
callId = callId, callId = callId,
callStatus = CallTileTimelineItem.CallStatus.REJECTED, callStatus = CallTileTimelineItem.CallStatus.REJECTED,
callKind = callKind, callKind = callKind,
callback = callback, callback = params.callback,
highlight = highlight, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = false isStillActive = false
) )
@ -104,8 +102,8 @@ class CallItemFactory @Inject constructor(
callId = callId, callId = callId,
callStatus = CallTileTimelineItem.CallStatus.ENDED, callStatus = CallTileTimelineItem.CallStatus.ENDED,
callKind = callKind, callKind = callKind,
callback = callback, callback = params.callback,
highlight = highlight, highlight = params.isHighlighted,
informationData = informationData, informationData = informationData,
isStillActive = false 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.DefaultItem_ import im.vector.app.features.home.room.detail.timeline.item.DefaultItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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 import javax.inject.Inject
class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider, class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider,
@ -43,8 +42,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
text = text, text = text,
itemLongClickListener = { view -> itemLongClickListener = { view ->
callback?.onEventLongClicked(informationData, null, view) ?: false callback?.onEventLongClicked(informationData, null, view) ?: false
}, }
readReceiptsCallback = callback
) )
return DefaultItem_() return DefaultItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
@ -52,16 +50,14 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
.attributes(attributes) .attributes(attributes)
} }
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams, throwable: Throwable? = null): DefaultItem {
highlight: Boolean, val event = params.event
callback: TimelineEventController.Callback?,
throwable: Throwable? = null): DefaultItem {
val text = if (throwable == null) { val text = if (throwable == null) {
stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType()) stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType())
} else { } else {
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId) stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId)
} }
val informationData = informationDataFactory.create(event, null, null) val informationData = informationDataFactory.create(params)
return create(text, informationData, highlight, callback) 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.ColorProvider
import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider 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.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory 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.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType 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.events.model.toModel
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import javax.inject.Inject import javax.inject.Inject
@ -46,11 +44,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val attributesFactory: MessageItemAttributesFactory, private val attributesFactory: MessageItemAttributesFactory,
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
prevEvent: TimelineEvent?, val event = params.event
nextEvent: TimelineEvent?,
highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
return when { return when {
@ -109,14 +104,14 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
} }
} }
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) val informationData = messageInformationDataFactory.create(params)
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback) val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, params.callback)
return MessageTextItem_() return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight) .highlighted(params.isHighlighted)
.attributes(attributes) .attributes(attributes)
.message(spannableStr) .message(spannableStr)
.movementMethod(createLinkMovementMethod(callback)) .movementMethod(createLinkMovementMethod(params.callback))
} }
else -> null 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.R
import im.vector.app.core.resources.StringProvider 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.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.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory 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.extensions.orFalse
import org.matrix.android.sdk.api.session.Session 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.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.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import javax.inject.Inject import javax.inject.Inject
@ -41,15 +39,14 @@ class EncryptionItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) { private val session: Session) {
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): StatusTileTimelineItem? {
highlight: Boolean, val event = params.event
callback: TimelineEventController.Callback?): StatusTileTimelineItem? {
if (!event.root.isStateEvent()) { if (!event.root.isStateEvent()) {
return null return null
} }
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(event, null, null) val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
val title: String val title: String
@ -86,7 +83,7 @@ class EncryptionItemFactory @Inject constructor(
readReceiptsCallback = attributes.readReceiptsCallback readReceiptsCallback = attributes.readReceiptsCallback
) )
) )
.highlighted(highlight) .highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline) .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.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener 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.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.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration 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.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem
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, class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val roomSummariesHolder: RoomSummariesHolder) { private val roomSummariesHolder: RoomSummariesHolder,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val collapsedEventIds = linkedSetOf<Long>() private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>() private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@ -85,12 +86,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
eventIdToHighlight: String?, eventIdToHighlight: String?,
requestModelBuild: () -> Unit, requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight)
return if (prevSameTypeEvents.isEmpty()) { return if (mergedEvents.isEmpty()) {
null null
} else { } else {
var highlighted = false var highlighted = false
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size) val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.forEach { mergedEvent -> mergedEvents.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
@ -126,8 +126,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
onCollapsedStateChanged = { onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it mergeItemCollapseStates[event.localId] = it
requestModelBuild() requestModelBuild()
}, }
readReceiptsCallback = callback
) )
MergedMembershipEventsItem_() MergedMembershipEventsItem_()
.id(mergeId) .id(mergeId)
@ -205,7 +204,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
}, },
hasEncryptionEvent = hasEncryption, hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback,
callback = callback, callback = callback,
currentUserId = currentUserId, currentUserId = currentUserId,
roomSummary = roomSummariesHolder.get(event.roomId), 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.OPTION_TYPE_POLL
import org.matrix.android.sdk.api.session.room.model.message.getFileName 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.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.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
@ -118,15 +117,13 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId) pillsPostProcessorFactory.create(roomId)
} }
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
prevEvent: TimelineEvent?, val event = params.event
nextEvent: TimelineEvent?, val highlight = params.isHighlighted
highlight: Boolean, val callback = params.callback
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
roomId = event.roomId roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) val informationData = messageInformationDataFactory.create(params)
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// message is redacted // message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback) 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 || 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 // 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) val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
@ -158,7 +155,7 @@ class MessageItemFactory @Inject constructor(
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildOptionsMessageItem(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) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
} }

View File

@ -17,13 +17,11 @@
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.AvatarRenderer 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.format.NoticeEventFormatter
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider 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.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
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 import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, 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 informationDataFactory: MessageInformationDataFactory,
private val avatarSizeProvider: AvatarSizeProvider) { private val avatarSizeProvider: AvatarSizeProvider) {
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): NoticeItem? {
highlight: Boolean, val event = params.event
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null val formattedText = eventFormatter.format(event) ?: return null
val informationData = informationDataFactory.create(event, null, null) val informationData = informationDataFactory.create(params)
val attributes = NoticeItem.Attributes( val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,
informationData = informationData, informationData = informationData,
noticeText = formattedText, noticeText = formattedText,
itemLongClickListener = { view -> itemLongClickListener = { view ->
callback?.onEventLongClicked(informationData, null, view) ?: false params.callback?.onEventLongClicked(informationData, null, view) ?: false
}, },
readReceiptsCallback = callback, readReceiptsCallback = params.callback,
avatarClickListener = { callback?.onAvatarClicked(informationData) } avatarClickListener = { params.callback?.onAvatarClicked(informationData) }
) )
return NoticeItem_() return NoticeItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight) .highlighted(params.isHighlighted)
.attributes(attributes) .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.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider 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 im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_
import me.gujun.android.span.span import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.Session 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.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent 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 import javax.inject.Inject
class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider, 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 session: Session,
private val noticeItemFactory: NoticeItemFactory) { 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 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 roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null
val text = span { val text = span {
+stringProvider.getString(R.string.room_tombstone_continuation_description) +stringProvider.getString(R.string.room_tombstone_continuation_description)
+"\n" +"\n"
span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) { span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) {
textDecorationLine = "underline" textDecorationLine = "underline"
onClick = { callback?.onRoomCreateLinkClicked(roomLink) } onClick = { params.callback?.onRoomCreateLinkClicked(roomLink) }
} }
} }
return RoomCreateItem_() return RoomCreateItem_()
.text(text) .text(text)
} }
private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { private fun defaultRendering(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
return if (userPreferencesProvider.shouldShowHiddenEvents()) { return if (userPreferencesProvider.shouldShowHiddenEvents()) {
noticeItemFactory.create(event, false, callback) noticeItemFactory.create(params)
} else { } else {
null 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.TimelineEmptyItem_ import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber import timber.log.Timber
@ -35,23 +34,21 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val widgetItemFactory: WidgetItemFactory, private val widgetItemFactory: WidgetItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory,
private val callItemFactory: CallItemFactory, private val callItemFactory: CallItemFactory,
private val userPreferencesProvider: UserPreferencesProvider) { private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
/** /**
* Reminder: nextEvent is older and prevEvent is newer. * Reminder: nextEvent is older and prevEvent is newer.
*/ */
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> {
prevEvent: TimelineEvent?, val event = params.event
nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try { val computedModel = try {
if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) {
return buildEmptyItem(event, params.prevEvent, params.highlightedEventId)
}
when (event.root.getClearType()) { when (event.root.getClearType()) {
// Message itemsX
EventType.STICKER, EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) EventType.MESSAGE -> messageItemFactory.create(params)
// State and call
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
@ -63,68 +60,61 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_GUEST_ACCESS,
EventType.STATE_ROOM_POWER_LEVELS, EventType.REDACTION,
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.STATE_ROOM_ALIASES, EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
EventType.REACTION,
EventType.CALL_CANDIDATES, EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES, EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER, EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE -> { EventType.CALL_NEGOTIATE,
// TODO These are not filtered out by timeline when encrypted EventType.REACTION,
// For now manually ignore EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params)
if (userPreferencesProvider.shouldShowHiddenEvents()) { EventType.STATE_ROOM_WIDGET_LEGACY,
noticeItemFactory.create(event, highlight, callback) 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 { } else {
null encryptedItemFactory.create(params)
} }
} }
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> { EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(event, highlight, callback) verificationConclusionItemFactory.create(params)
} }
// Unhandled event types // Unhandled event types
else -> { else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON // Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled") Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(event, highlight, callback) defaultItemFactory.create(params)
} }
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable, "failed to create message item") 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_() return TimelineEmptyItem_()
.id(timelineEvent.localId) .id(timelineEvent.localId)
.eventId(timelineEvent.eventId) .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.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider 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.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.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory 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.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent 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.model.message.MessageVerificationCancelContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -54,37 +52,35 @@ class VerificationItemFactory @Inject constructor(
private val session: Session private val session: Session
) { ) {
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
highlight: Boolean, val event = params.event
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (event.root.eventId == null) return null if (event.root.eventId == null) return null
val relContent: MessageRelationContent = event.root.content.toModel() val relContent: MessageRelationContent = event.root.content.toModel()
?: event.root.getClearContent().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 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 // If we cannot find the referenced request we do not display the done event
val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId) 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 it's not a request ignore this event
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback) // 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 informationData = messageInformationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
when (event.root.getClearType()) { when (event.root.getClearType()) {
EventType.KEY_VERIFICATION_CANCEL -> { EventType.KEY_VERIFICATION_CANCEL -> {
// Is the request referenced is actually really cancelled? // Is the request referenced is actually really cancelled?
val cancelContent = event.root.getClearContent().toModel<MessageVerificationCancelContent>() val cancelContent = event.root.getClearContent().toModel<MessageVerificationCancelContent>()
?: return ignoredConclusion(event, highlight, callback) ?: return ignoredConclusion(params)
when (safeValueOf(cancelContent.code)) { when (safeValueOf(cancelContent.code)) {
CancelCode.MismatchedCommitment, CancelCode.MismatchedCommitment,
@ -107,22 +103,22 @@ class VerificationItemFactory @Inject constructor(
readReceiptsCallback = attributes.readReceiptsCallback readReceiptsCallback = attributes.readReceiptsCallback
) )
) )
.highlighted(highlight) .highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
else -> return ignoredConclusion(event, highlight, callback) else -> return ignoredConclusion(params)
} }
} }
EventType.KEY_VERIFICATION_DONE -> { EventType.KEY_VERIFICATION_DONE -> {
// Is the request referenced is actually really completed? // Is the request referenced is actually really completed?
if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) { if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) {
return ignoredConclusion(event, highlight, callback) return ignoredConclusion(params)
} }
// We only tale the one sent by me // We only tale the one sent by me
if (informationData.sentByMe) { if (informationData.sentByMe) {
// We only display the done sent by the other user, the done send by me is ignored // 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_() return StatusTileTimelineItem_()
.attributes( .attributes(
@ -140,18 +136,15 @@ class VerificationItemFactory @Inject constructor(
readReceiptsCallback = attributes.readReceiptsCallback readReceiptsCallback = attributes.readReceiptsCallback
) )
) )
.highlighted(highlight) .highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
} }
} }
return null return null
} }
private fun ignoredConclusion(event: TimelineEvent, private fun ignoredConclusion(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
highlight: Boolean, if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(params)
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
return null return null
} }
} }

View File

@ -20,7 +20,6 @@ import im.vector.app.ActiveSessionDataSource
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.StringProvider 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.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory 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.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Event 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.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.WidgetContent
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import javax.inject.Inject import javax.inject.Inject
@ -47,25 +45,24 @@ class WidgetItemFactory @Inject constructor(
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
fun create(event: TimelineEvent, fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
highlight: Boolean, val event = params.event
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { 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 // 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, private fun createJitsiItem(params: TimelineItemFactoryParams,
callback: TimelineEventController.Callback?,
widgetContent: WidgetContent, widgetContent: WidgetContent,
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
val informationData = informationDataFactory.create(timelineEvent, null, null) val timelineEvent = params.event
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val informationData = informationDataFactory.create(params)
val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
val message = if (widgetContent.isActive()) { 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.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime 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.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData 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.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.settings.VectorPreferences 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 dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData { fun create(params: TimelineItemFactoryParams): MessageInformationData {
// Non nullability has been tested before val event = params.event
val eventId = event.root.eventId!! val nextEvent = params.nextEvent
val eventId = event.eventId
val date = event.root.localDateTime() val date = event.root.localDateTime()
val nextDate = nextEvent?.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 isSentByMe = event.root.senderId == session.myUserId
val sendStateDecoration = if (isSentByMe) { val sendStateDecoration = if (isSentByMe) {
getSendStateDecoration( getSendStateDecoration(
eventSendState = event.root.sendState, event = event,
prevEventSendState = prevEvent?.root?.sendState, lastSentEventWithoutReadReceipts = params.lastSentEventIdWithoutReadReceipts,
anyReadReceipts = event.readReceipts.any { it.user.userId != session.myUserId },
isMedia = event.root.isAttachmentMessage() isMedia = event.root.isAttachmentMessage()
) )
} else { } else {
@ -111,15 +111,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
}, },
hasBeenEdited = event.hasBeenEdited(), hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, 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 -> referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState
?: VerificationState.REQUEST ?: VerificationState.REQUEST
@ -131,15 +122,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
) )
} }
private fun getSendStateDecoration(eventSendState: SendState, private fun getSendStateDecoration(event: TimelineEvent,
prevEventSendState: SendState?, lastSentEventWithoutReadReceipts: String?,
anyReadReceipts: Boolean,
isMedia: Boolean): SendStateDecoration { isMedia: Boolean): SendStateDecoration {
val eventSendState = event.root.sendState
return if (eventSendState.isSending()) { return if (eventSendState.isSending()) {
if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA
} else if (eventSendState.hasFailed()) { } else if (eventSendState.hasFailed()) {
SendStateDecoration.FAILED SendStateDecoration.FAILED
} else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) { } else if (lastSentEventWithoutReadReceipts == event.eventId) {
SendStateDecoration.SENT SendStateDecoration.SENT
} else { } else {
SendStateDecoration.NONE SendStateDecoration.NONE

View File

@ -20,13 +20,14 @@ import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem_ 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.UnreadState 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.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem 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.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ 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 org.matrix.android.sdk.api.session.room.timeline.Timeline
import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KMutableProperty0
@ -34,7 +35,7 @@ private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>, class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
private val adapterPositionMapping: MutableMap<String, Int>, private val adapterPositionMapping: MutableMap<String, Int>,
private val vectorPreferences: VectorPreferences, private val userPreferencesProvider: UserPreferencesProvider,
private val callManager: WebRtcCallManager private val callManager: WebRtcCallManager
) { ) {
@ -56,23 +57,39 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
models.addForwardPrefetchIfNeeded(timeline, callback) models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator() val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
var index = 0 var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId 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 // Then iterate on models so we have the exact positions in the adapter
modelsIterator.forEach { epoxyModel -> modelsIterator.forEach { epoxyModel ->
if (epoxyModel is ItemWithEvents) { if (epoxyModel is ItemWithEvents) {
if (epoxyModel.isVisible()) {
atLeastOneVisibleItemSinceLastDaySeparator = true
atLeastOneVisibleItemsBeforeReadMarker = true
}
epoxyModel.getEventIds().forEach { eventId -> epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index adapterPositionMapping[eventId] = index
if (eventId == firstUnreadEventId) { if (epoxyModel.canAppendReadMarker() && eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) {
modelsIterator.addReadMarkerItem(callback) modelsIterator.addReadMarkerItem(callback)
index++ index++
positionOfReadMarker.set(index) positionOfReadMarker.set(index)
} }
} }
} }
if (epoxyModel is CallTileTimelineItem) { if (epoxyModel is DaySeparatorItem) {
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents) 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++ index++
} }
@ -94,20 +111,23 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
epoxyModel: CallTileTimelineItem, epoxyModel: CallTileTimelineItem,
callIds: MutableSet<String>, callIds: MutableSet<String>,
showHiddenEvents: Boolean showHiddenEvents: Boolean
) { ): Boolean {
val callId = epoxyModel.attributes.callId val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or // 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) // if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId) val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) { val removed = shouldRemoveCallItem && !showHiddenEvents
if (removed) {
remove() remove()
val emptyItem = TimelineEmptyItem_() val emptyItem = TimelineEmptyItem_()
.id(epoxyModel.id()) .id(epoxyModel.id())
.eventId(epoxyModel.attributes.informationData.eventId) .eventId(epoxyModel.attributes.informationData.eventId)
.notBlank(false)
add(emptyItem) add(emptyItem)
} }
callIds.add(callId) callIds.add(callId)
return removed
} }
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { 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 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.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
object TimelineDisplayableEvents { 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( val DISPLAYABLE_TYPES = listOf(
EventType.MESSAGE, EventType.MESSAGE,
EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET_LEGACY,
@ -68,7 +70,7 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_POWER_LEVELS, EventType.STATE_ROOM_POWER_LEVELS,
EventType.STATE_ROOM_ENCRYPTION -> true 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), // 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 // but exclude events where the room creator invite others, or where others join
roomCreatorUserId != null && root.stateKey == roomCreatorUserId roomCreatorUserId != null && root.stateKey == roomCreatorUserId
@ -76,39 +78,3 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
else -> false 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 package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.resources.UserPreferencesProvider 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 org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import javax.inject.Inject import javax.inject.Inject
class TimelineSettingsFactory @Inject constructor( class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
private val userPreferencesProvider: UserPreferencesProvider,
private val session: Session
) {
fun create(): TimelineSettings { fun create(): TimelineSettings {
return if (userPreferencesProvider.shouldShowHiddenEvents()) { return TimelineSettings(
TimelineSettings( initialSize = 30,
initialSize = 30, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
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
)
}
} }
} }

View File

@ -24,7 +24,6 @@ import androidx.annotation.IdRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.ui.views.ShieldImageView 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.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider 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.TimelineEventController
@ -41,10 +40,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
abstract val baseAttributes: Attributes abstract val baseAttributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
})
private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) { override fun onReacted(reactionButton: ReactionButton) {
baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true) baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
@ -69,12 +64,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
holder.readReceiptsView.render(
baseAttributes.informationData.readReceipts,
baseAttributes.avatarRenderer,
_readReceiptsClickListener
)
val reactions = baseAttributes.informationData.orderedReactionList val reactions = baseAttributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false holder.reactionsContainer.isVisible = false
@ -111,7 +100,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
override fun unbind(holder: H) { override fun unbind(holder: H) {
holder.reactionsContainer.setOnLongClickListener(null) holder.reactionsContainer.setOnLongClickListener(null)
holder.readReceiptsView.unbind(baseAttributes.avatarRenderer)
super.unbind(holder) 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.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.platform.CheckableView import im.vector.app.core.platform.CheckableView
import im.vector.app.core.ui.views.ReadReceiptsView
import im.vector.app.core.utils.DimensionConverter 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() { abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<View>(R.id.messageStartGuideline) val leftGuideline by bind<View>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground) val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
override fun bindView(itemView: View) { override fun bindView(itemView: View) {
super.bindView(itemView) 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.view.View
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer 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 import org.matrix.android.sdk.api.util.MatrixItem
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() { 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.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse) holder.expandView.setText(R.string.merged_events_collapse)
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
} }
protected val distinctMergeData by lazy { protected val distinctMergeData by lazy {
@ -72,7 +68,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
val isCollapsed: Boolean val isCollapsed: Boolean
val mergeData: List<Data> val mergeData: List<Data>
val avatarRenderer: AvatarRenderer val avatarRenderer: AvatarRenderer
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
val onCollapsedStateChanged: (Boolean) -> Unit val onCollapsedStateChanged: (Boolean) -> Unit
} }

View File

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

View File

@ -22,4 +22,8 @@ interface ItemWithEvents {
* Will generally get only one, but it handles the merged items. * Will generally get only one, but it handles the merged items.
*/ */
fun getEventIds(): List<String> 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.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer 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) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() { abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
@ -56,8 +54,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
holder.avatarListView.visibility = View.INVISIBLE holder.avatarListView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE holder.summaryView.visibility = View.GONE
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
} }
class Holder : BasedMergedItem.Holder(STUB_ID) { class Holder : BasedMergedItem.Holder(STUB_ID) {
@ -73,7 +69,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
override val isCollapsed: Boolean, override val isCollapsed: Boolean,
override val mergeData: List<Data>, override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit override val onCollapsedStateChanged: (Boolean) -> Unit
) : BasedMergedItem.Attributes ) : BasedMergedItem.Attributes
} }

View File

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

View File

@ -36,10 +36,8 @@ data class MessageInformationData(
/*List of reactions (emoji,count,isSelected)*/ /*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val pollResponseAggregatedSummary: PollResponseData? = null, val pollResponseAggregatedSummary: PollResponseData? = null,
val hasBeenEdited: Boolean = false, val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false, val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null, val referencesInfoData: ReferencesInfoData? = null,
val sentByMe: Boolean, val sentByMe: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE, 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.ClickListener
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.ui.views.ShieldImageView 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.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
@ -36,16 +35,11 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.noticeTextView.text = attributes.noticeText holder.noticeTextView.text = attributes.noticeText
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
holder.avatarImageView.onClick(attributes.avatarClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener)
when (attributes.informationData.e2eDecoration) { when (attributes.informationData.e2eDecoration) {
@ -62,7 +56,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
attributes.avatarRenderer.clear(holder.avatarImageView) attributes.avatarRenderer.clear(holder.avatarImageView)
holder.readReceiptsView.unbind(attributes.avatarRenderer)
super.unbind(holder) 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) 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. * 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"?> <?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android" <View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" /> android:layout_height="0dp" />

View File

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

View File

@ -10,7 +10,7 @@
android:id="@+id/messageSelectedBackground" android:id="@+id/messageSelectedBackground"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignBottom="@+id/readReceiptsView" android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:background="@drawable/highlighted_message_background" /> android:background="@drawable/highlighted_message_background" />
@ -80,14 +80,4 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> 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> </RelativeLayout>

View File

@ -120,14 +120,6 @@
</com.google.android.flexbox.FlexboxLayout> </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> </LinearLayout>
</RelativeLayout> </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 <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_ROOM_MEMBER_STATE_EVENTS_KEY" android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"
android:summary="@string/settings_show_room_member_state_events_summary" android:summary="@string/settings_show_join_leave_messages_summary"
android:title="@string/settings_show_room_member_state_events" /> 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 <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"