Timeline: fix SendState decoration + some filtering issues

This commit is contained in:
ganfra 2021-03-30 18:07:05 +02:00
parent 2b93367165
commit 13cb81b92f
21 changed files with 209 additions and 181 deletions

View File

@ -26,12 +26,15 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute var visible: Boolean = false
@EpoxyAttribute var notBlank: Boolean = false
override fun isVisible() = false
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.updateLayoutParams {
this.height = if (visible) 1 else 0
// Force height to 1px so scrolling works correctly
this.height = if (notBlank) 1 else 0
}
}

View File

@ -32,9 +32,7 @@ import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState
@ -47,6 +45,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineControlle
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
@ -55,7 +54,6 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer
@ -337,13 +335,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return
}
val receiptsByEvents = getReadReceiptsByShownEvent()
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
(0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position)
val params = TimelineItemFactoryParams(
event = event,
prevEvent = prevEvent,
nextEvent = nextEvent,
highlightedEventId = eventIdToHighlight,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback
)
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
modelCache[position] = buildCacheItem(event, nextEvent, prevEvent)
modelCache[position] = buildCacheItem(params)
}
val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed
@ -351,15 +358,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
private fun buildCacheItem(event: TimelineEvent,
nextEvent: TimelineEvent?,
prevEvent: TimelineEvent?
): CacheItemData {
private fun buildCacheItem(params: TimelineItemFactoryParams): CacheItemData {
val event = params.event
if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId)
}
updateUTDStates(event, nextEvent)
val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also {
updateUTDStates(event, params.nextEvent)
val eventModel = timelineItemFactory.create(params).also {
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
@ -399,13 +404,37 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
)
}
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>>()
var lastShownEventId: String? = null
val itr = currentSnapshot.listIterator(currentSnapshot.size)
while (itr.hasPrevious()) {
val event = itr.previous()
val currentReadReceipts = ArrayList(event.readReceipts)
val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId
}
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
lastShownEventId = event.eventId
}

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
val prevSameTypeEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2)
val prevSameTypeEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight)
return if (prevSameTypeEvents.isEmpty()) {
null
} else {

View File

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

View File

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

View File

@ -22,27 +22,21 @@ 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.Session
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import javax.inject.Inject
class ReadReceiptsItemFactory @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer) {
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
.asSequence()
.filter {
it.user.userId != session.myUserId
}
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
.toList()
if (readReceiptsData.isEmpty()) {
return null
}
return ReadReceiptsItem_()
.id("read_receipts_$eventId")
.eventId(eventId)

View File

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

View File

@ -19,8 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -41,20 +39,16 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
/**
* Reminder: nextEvent is older and prevEvent is newer.
*/
fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?,
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> {
val event = params.event
val computedModel = try {
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
return buildEmptyItem(event, prevEvent, eventIdToHighlight)
if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) {
return buildEmptyItem(event, params.prevEvent, params.highlightedEventId)
}
when (event.root.getClearType()) {
// Message items
// Message itemsX
EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
EventType.MESSAGE -> messageItemFactory.create(params)
EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
@ -78,49 +72,49 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params)
EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.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(event, callback)
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params)
// Calls
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback)
EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto
EventType.ENCRYPTED -> {
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
messageItemFactory.create(params)
} else {
encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
encryptedItemFactory.create(params)
}
}
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(event, highlight, callback)
verificationConclusionItemFactory.create(params)
}
// Unhandled event types
else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON
Timber.v("Type ${event.root.getClearType()} not handled")
defaultItemFactory.create(event, highlight, callback)
defaultItemFactory.create(params)
}
}
} catch (throwable: Throwable) {
Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(event, highlight, callback, throwable)
defaultItemFactory.create(params, throwable)
}
return computedModel ?: buildEmptyItem(event, prevEvent, eventIdToHighlight)
return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId)
}
private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, eventIdToHighlight: String?): TimelineEmptyItem {
val makesEmptyItemVisible = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, eventIdToHighlight)
private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?): TimelineEmptyItem {
val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId)
return TimelineEmptyItem_()
.id(timelineEvent.localId)
.eventId(timelineEvent.eventId)
.visible(makesEmptyItemVisible)
.notBlank(isNotBlank)
}
}

View File

@ -0,0 +1,33 @@
/*
* 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: Boolean
get() = highlightedEventId == event.eventId
}

View File

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

View File

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

View File

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

View File

@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.helper
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.webrtc.WebRtcCallManager
@ -29,7 +28,6 @@ import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineIte
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import kotlin.reflect.KMutableProperty0
@ -67,30 +65,29 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
// Then iterate on models so we have the exact positions in the adapter
modelsIterator.forEach { epoxyModel ->
if(epoxyModel !is TimelineEmptyItem){
atLeastOneVisibleItemSinceLastDaySeparator = true
atLeastOneVisibleItemsBeforeReadMarker = true
}
if (epoxyModel is ItemWithEvents) {
if (epoxyModel.isVisible()) {
atLeastOneVisibleItemSinceLastDaySeparator = true
atLeastOneVisibleItemsBeforeReadMarker = true
}
epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index
if (eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) {
if (eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker && epoxyModel.canAppendReadMarker()) {
modelsIterator.addReadMarkerItem(callback)
index++
positionOfReadMarker.set(index)
}
}
}
if(epoxyModel is DaySeparatorItem){
if(!atLeastOneVisibleItemSinceLastDaySeparator){
if (epoxyModel is DaySeparatorItem) {
if (!atLeastOneVisibleItemSinceLastDaySeparator) {
modelsIterator.remove()
return@forEach
}
atLeastOneVisibleItemSinceLastDaySeparator = false
}
else if (epoxyModel is CallTileTimelineItem) {
} else if (epoxyModel is CallTileTimelineItem) {
val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
if(!hasBeenRemoved){
if (!hasBeenRemoved) {
atLeastOneVisibleItemSinceLastDaySeparator = true
}
}
@ -126,7 +123,7 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
val emptyItem = TimelineEmptyItem_()
.id(epoxyModel.id())
.eventId(epoxyModel.attributes.informationData.eventId)
.visible(false)
.notBlank(false)
add(emptyItem)
}
callIds.add(callId)

View File

@ -29,7 +29,7 @@ import javax.inject.Inject
class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int): List<TimelineEvent> {
fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?): List<TimelineEvent> {
if (index >= timelineEvents.size - 1) {
return emptyList()
}
@ -51,30 +51,30 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
} else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
}
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it)}
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight)}
if (filteredSameTypeEvents.size < minSize) {
return emptyList()
}
return filteredSameTypeEvents
}
fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int): List<TimelineEvent> {
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)
nextSameTypeEvents(it, 0, minSize, eventIdToHighlight)
}
.reversed()
}
fun shouldShowEvent(timelineEvent: TimelineEvent, highlightEventId: String? = null): Boolean {
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 == highlightEventId) {
if (timelineEvent.eventId == highlightedEventId) {
return true
}
if (!timelineEvent.isDisplayable()) {

View File

@ -44,6 +44,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
lateinit var dimensionConverter: DimensionConverter
@CallSuper
override fun bind(holder: H) {
super.bind(holder)

View File

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

View File

@ -33,6 +33,8 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
@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) {