mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge branch 'develop' into feature/bma/cleanup
This commit is contained in:
commit
295be5286b
@ -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 🧱:
|
||||||
-
|
-
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 ->
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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? {
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()) {
|
||||||
|
@ -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
|
||||||
|
@ -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?) {
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user