Timeline: refact epoxy attributes

This commit is contained in:
ganfra 2019-09-11 18:04:17 +02:00
parent 51a4c93676
commit b8ebe3570b
13 changed files with 286 additions and 161 deletions

View File

@ -32,11 +32,13 @@ import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.media.ImageContentRenderer
@ -47,7 +49,7 @@ import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val avatarRenderer: AvatarRenderer,
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
@TimelineEventControllerHandler
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
@ -89,8 +91,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onUrlLongClicked(url: String): Boolean
}
private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
@ -231,7 +231,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
return modelCache
.map {
val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) {
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) {
null
} else {
it.eventModel
@ -255,7 +255,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition)
val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback){
requestModelBuild()
}
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
@ -270,53 +272,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
}
// TODO Phase 3 Handle the case where the eventId we have to highlight is merged
private fun buildMergedHeaderItem(event: TimelineEvent,
nextEvent: TimelineEvent?,
items: List<TimelineEvent>,
addDaySeparator: Boolean,
currentPosition: Int): MergedHeaderItem? {
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
null
} else {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
if (prevSameTypeEvents.isEmpty()) {
null
} else {
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = mergedEvents.map { mergedEvent ->
val senderAvatar = mergedEvent.senderAvatar()
val senderName = mergedEvent.senderName()
MergedHeaderItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName ?: "",
eventId = mergedEvent.localId
)
}
val mergedEventIds = mergedEvents.map { it.localId }
// We try to find if one of the item id were used as mergeItemCollapseStates key
// => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds)
} else {
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
MergedHeaderItem(isCollapsed, mergeId, mergedData, avatarRenderer) {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
}
}
}
/**
* Return true if added
*/

View File

@ -17,11 +17,12 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_
import javax.inject.Inject
class DefaultItemFactory @Inject constructor(){
class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider) {
fun create(event: TimelineEvent, highlight: Boolean, exception: Exception? = null): DefaultItem? {
val text = if (exception == null) {
@ -30,8 +31,10 @@ class DefaultItemFactory @Inject constructor(){
"an exception occurred when rendering the event ${event.root.eventId}"
}
return DefaultItem_()
.text(text)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.text(text)
}
}

View File

@ -23,7 +23,6 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
@ -55,7 +54,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
}
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
val spannableStr = span(message) {
textStyle = "italic"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
@ -66,11 +65,10 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
val informationData = messageInformationDataFactory.create(event, nextEvent)
val attributes = attributesFactory.create(null, informationData, callback)
return MessageTextItem_()
.highlighted(highlight)
.attributes(attributes)
.message(spannableStr)
.highlighted(highlight)
.urlClickCallback(callback)
}
else -> null
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2019 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.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider) {
private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
items: List<TimelineEvent>,
addDaySeparator: Boolean,
currentPosition: Int,
callback: TimelineEventController.Callback?,
requestModelBuild: () -> Unit)
: MergedHeaderItem? {
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
null
} else {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
if (prevSameTypeEvents.isEmpty()) {
null
} else {
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = mergedEvents.map { mergedEvent ->
val senderAvatar = mergedEvent.senderAvatar()
val senderName = mergedEvent.senderName()
MergedHeaderItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName ?: "",
eventId = mergedEvent.localId
)
}
val mergedEventIds = mergedEvents.map { it.localId }
// We try to find if one of the item id were used as mergeItemCollapseStates key
// => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds)
} else {
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedHeaderItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}
)
MergedHeaderItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
}
}
}
fun isCollapsed(localId: Long): Boolean {
return collapsedEventIds.contains(localId)
}
}

View File

@ -41,16 +41,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
@ -70,7 +69,8 @@ class MessageItemFactory @Inject constructor(
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val noticeItemFactory: NoticeItemFactory) {
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider) {
fun create(event: TimelineEvent,
@ -90,11 +90,11 @@ class MessageItemFactory @Inject constructor(
val messageContent: MessageContent =
event.getLastMessageContent()
?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
if (messageContent.relatesTo?.type == RelationType.REPLACE
|| 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 it when debugging as a notice event
return noticeItemFactory.create(event, highlight, callback)
@ -105,15 +105,15 @@ class MessageItemFactory @Inject constructor(
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
informationData,
highlight,
callback,
attributes)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback,
attributes)
informationData,
highlight,
callback,
attributes)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -131,6 +131,7 @@ class MessageItemFactory @Inject constructor(
return MessageFileItem_()
.attributes(attributes)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.filename(messageContent.body)
.iconRes(R.drawable.filetype_audio)
.clickListener(
@ -146,6 +147,7 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_()
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.filename(messageContent.body)
.iconRes(R.drawable.filetype_attachment)
@ -158,6 +160,7 @@ class MessageItemFactory @Inject constructor(
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
val text = "${messageContent.type} message events are not yet handled"
return DefaultItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.text(text)
.highlighted(highlight)
}
@ -182,6 +185,7 @@ class MessageItemFactory @Inject constructor(
)
return MessageImageVideoItem_()
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.info?.mimeType == "image/gif")
@ -203,7 +207,7 @@ class MessageItemFactory @Inject constructor(
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
@ -220,6 +224,7 @@ class MessageItemFactory @Inject constructor(
)
return MessageImageVideoItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
@ -250,6 +255,7 @@ class MessageItemFactory @Inject constructor(
message(linkifiedBody)
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.urlClickCallback(callback)
@ -282,9 +288,9 @@ class MessageItemFactory @Inject constructor(
//nop
}
},
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable
}
@ -303,6 +309,7 @@ class MessageItemFactory @Inject constructor(
linkifyBody(formattedBody, callback)
}
return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.message(message)
.highlighted(highlight)
@ -328,6 +335,7 @@ class MessageItemFactory @Inject constructor(
message(message)
}
}
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
.urlClickCallback(callback)
@ -336,6 +344,7 @@ class MessageItemFactory @Inject constructor(
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
highlight: Boolean): RedactedMessageItem? {
return RedactedMessageItem_()
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
.highlighted(highlight)
}

View File

@ -20,28 +20,34 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
private val avatarRenderer: AvatarRenderer,
private val informationDataFactory: MessageInformationDataFactory) {
class NoticeItemFactory @Inject constructor(
private val eventFormatter: NoticeEventFormatter,
private val avatarRenderer: AvatarRenderer,
private val informationDataFactory: MessageInformationDataFactory,
private val avatarSizeProvider: AvatarSizeProvider
) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
val informationData = informationDataFactory.create(event, null)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,
informationData = informationData,
noticeText = formattedText,
callback = callback
)
return NoticeItem_()
.avatarRenderer(avatarRenderer)
.noticeText(formattedText)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight)
.informationData(informationData)
.baseCallback(callback)
.readReceiptsCallback(callback)
.attributes(attributes)
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019 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.riotx.features.home.room.detail.timeline.helper
import androidx.appcompat.app.AppCompatActivity
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import javax.inject.Inject
class AvatarSizeProvider @Inject constructor(private val context: AppCompatActivity) {
private val avatarStyle = AvatarStyle.SMALL
val leftGuideline: Int by lazy {
dpToPx(avatarStyle.avatarSizeDP + 8, context)
}
val avatarSize: Int by lazy {
dpToPx(avatarStyle.avatarSizeDP, context)
}
companion object {
enum class AvatarStyle(val avatarSizeDP: Int) {
BIG(50),
MEDIUM(40),
SMALL(30),
NONE(0)
}
}
}

View File

@ -31,10 +31,15 @@ import javax.inject.Inject
class MessageItemAttributesFactory @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider,
private val avatarSizeProvider: AvatarSizeProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider) {
fun create(messageContent: MessageContent?, informationData: MessageInformationData, callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
fun create(messageContent: MessageContent?,
informationData: MessageInformationData,
callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
return AbsMessageItem.Attributes(
avatarSize = avatarSizeProvider.avatarSize,
informationData = informationData,
avatarRenderer = avatarRenderer,
colorProvider = colorProvider,

View File

@ -81,9 +81,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
super.bind(holder)
if (attributes.informationData.showInformation) {
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context)
height = size
width = size
height = attributes.avatarSize
width = attributes.avatarSize
}
holder.avatarImageView.visibility = View.VISIBLE
holder.avatarImageView.setOnClickListener(_avatarClickListener)
@ -162,10 +161,21 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed()
}
abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
var reactionWrapper: ViewGroup? = null
var reactionFlowHelper: Flow? = null
}
/**
* This class holds all the common attributes for message items.
* This class holds all the common attributes for timeline items.
*/
data class Attributes(
val avatarSize: Int,
val informationData: MessageInformationData,
val avatarRenderer: AvatarRenderer,
val colorProvider: ColorProvider,
@ -178,14 +188,4 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
val emojiTypeFace: Typeface? = null
)
abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
var reactionWrapper: ViewGroup? = null
var reactionFlowHelper: Flow? = null
}
}

View File

@ -24,28 +24,23 @@ import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.platform.CheckableView
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import org.w3c.dom.Attr
/**
* Children must override getViewType()
*/
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
// To use for instance when opening a permalink with an eventId
@EpoxyAttribute
var highlighted: Boolean = false
@EpoxyAttribute
open var leftGuideline: Int = 0
override fun bind(holder: H) {
super.bind(holder)
//optimize?
val px = dpToPx(avatarStyle.avatarSizeDP + 8, holder.view.context)
holder.leftGuideline.setGuidelineBegin(px)
holder.leftGuideline.setGuidelineBegin(leftGuideline)
holder.checkableBackground.isChecked = highlighted
}
@ -63,13 +58,4 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
}
}
companion object {
enum class AvatarStyle(val avatarSizeDP: Int) {
BIG(50),
MEDIUM(40),
SMALL(30),
NONE(0)
}
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright 2019 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.riotx.features.home.room.detail.timeline.item

View File

@ -21,29 +21,20 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.children
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
data class MergedHeaderItem(private val isCollapsed: Boolean,
private val mergeId: String,
private val mergeData: List<Data>,
private val avatarRenderer: AvatarRenderer,
private val onCollapsedStateChanged: (Boolean) -> Unit
) : BaseEventItem<MergedHeaderItem.Holder>() {
@EpoxyAttribute
lateinit var attributes: Attributes
private val distinctMergeData = mergeData.distinctBy { it.userId }
init {
id(mergeId)
}
override fun getDefaultLayout(): Int {
return R.layout.item_timeline_event_base_noinfo
}
override fun createNewHolder(): Holder {
return Holder()
private val distinctMergeData by lazy {
attributes.mergeData.distinctBy { it.userId }
}
override fun getViewType() = STUB_ID
@ -51,10 +42,10 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
override fun bind(holder: Holder) {
super.bind(holder)
holder.expandView.setOnClickListener {
onCollapsedStateChanged(!isCollapsed)
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
}
if (isCollapsed) {
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, mergeData.size, mergeData.size)
if (attributes.isCollapsed) {
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
holder.summaryView.text = summary
holder.summaryView.visibility = View.VISIBLE
holder.avatarListView.visibility = View.VISIBLE
@ -62,7 +53,7 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
val data = distinctMergeData.getOrNull(index)
if (data != null && view is ImageView) {
view.visibility = View.VISIBLE
avatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view)
attributes.avatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view)
} else {
view.visibility = View.GONE
}
@ -84,6 +75,13 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
val avatarUrl: String?
)
data class Attributes(
val isCollapsed: Boolean,
val mergeData: List<Data>,
val avatarRenderer: AvatarRenderer,
val onCollapsedStateChanged: (Boolean) -> Unit
)
class Holder : BaseHolder(STUB_ID) {
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)

View File

@ -32,47 +32,38 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var noticeText: CharSequence? = null
@EpoxyAttribute
lateinit var informationData: MessageInformationData
@EpoxyAttribute
var baseCallback: TimelineEventController.BaseCallback? = null
lateinit var attributes: Attributes
private var longClickListener = View.OnLongClickListener {
return@OnLongClickListener baseCallback?.onEventLongClicked(informationData, null, it) == true
return@OnLongClickListener attributes.callback?.onEventLongClicked(attributes.informationData, null, it) == true
}
@EpoxyAttribute
var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts)
readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
private val _readMarkerCallback = object : ReadMarkerView.Callback {
override fun onReadMarkerDisplayed() {
readReceiptsCallback?.onReadMarkerLongDisplayed(informationData)
readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData)
}
}
override fun bind(holder: Holder) {
super.bind(holder)
holder.noticeTextView.text = noticeText
avatarRenderer.render(
informationData.avatarUrl,
informationData.senderId,
informationData.memberName?.toString()
?: informationData.senderId,
holder.noticeTextView.text = attributes.noticeText
attributes.avatarRenderer.render(
attributes.informationData.avatarUrl,
attributes.informationData.senderId,
attributes.informationData.memberName?.toString()
?: attributes.informationData.senderId,
holder.avatarImageView
)
holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener)
holder.readMarkerView.bindView(informationData, _readMarkerCallback)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback)
}
override fun unbind(holder: Holder) {
@ -89,6 +80,13 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
}
data class Attributes(
val avatarRenderer: AvatarRenderer,
val informationData: MessageInformationData,
val noticeText: CharSequence,
val callback: TimelineEventController.BaseCallback? = null
)
companion object {
private const val STUB_ID = R.id.messageContentNoticeStub
}