From b8ebe3570bb55733c8eedfbc8b399f1af835e029 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Sep 2019 18:04:17 +0200 Subject: [PATCH] Timeline: refact epoxy attributes --- .../timeline/TimelineEventController.kt | 59 ++-------- .../timeline/factory/DefaultItemFactory.kt | 7 +- .../timeline/factory/EncryptedItemFactory.kt | 6 +- .../factory/MergedHeaderItemFactory.kt | 103 ++++++++++++++++++ .../timeline/factory/MessageItemFactory.kt | 45 +++++--- .../timeline/factory/NoticeItemFactory.kt | 24 ++-- .../timeline/helper/AvatarSizeProvider.kt | 46 ++++++++ .../helper/MessageItemAttributesFactory.kt | 7 +- .../detail/timeline/item/AbsMessageItem.kt | 28 ++--- .../detail/timeline/item/BaseEventItem.kt | 22 +--- .../timeline/item/EventItemAttributes.kt | 18 +++ .../detail/timeline/item/MergedHeaderItem.kt | 42 ++++--- .../room/detail/timeline/item/NoticeItem.kt | 40 ++++--- 13 files changed, 286 insertions(+), 161 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index ffc573a634..9b9172a6f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -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() - private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() private var currentSnapshot: List = 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, - 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 */ diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 05e4007e04..fbe77d6ac2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -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) + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 938ac4673e..82ac4dc4d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt new file mode 100644 index 0000000000..06514e5973 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -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() + private val mergeItemCollapseStates = HashMap() + + fun create(event: TimelineEvent, + nextEvent: TimelineEvent?, + items: List, + 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) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 57baf4fee8..24d5abfd94 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.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() 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) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 6955cf3593..8663f87409 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt new file mode 100644 index 0000000000..9fcfdcfdf6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt @@ -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) + } + + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt index 47b5094c95..d69676cb2f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 5431b4ca85..913b1be466 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -81,9 +81,8 @@ abstract class AbsMessageItem : BaseEventItem() { 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 : BaseEventItem() { failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed() } + abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { + val avatarImageView by bind(R.id.messageAvatarImageView) + val memberNameView by bind(R.id.messageMemberNameView) + val timeView by bind(R.id.messageTimeView) + val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(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 : BaseEventItem() { val emojiTypeFace: Typeface? = null ) - abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { - val avatarImageView by bind(R.id.messageAvatarImageView) - val memberNameView by bind(R.id.messageMemberNameView) - val timeView by bind(R.id.messageTimeView) - val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) - var reactionWrapper: ViewGroup? = null - var reactionFlowHelper: Flow? = null - } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 5621f6047a..efdc6d06c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -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 : VectorEpoxyModel() { - 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 : VectorEpoxyModel } } - companion object { - - enum class AvatarStyle(val avatarSizeDP: Int) { - BIG(50), - MEDIUM(40), - SMALL(30), - NONE(0) - } - } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt new file mode 100644 index 0000000000..5c1ab5b347 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/EventItemAttributes.kt @@ -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 + diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 4f26f9bb11..a36d9ab7cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -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() { -data class MergedHeaderItem(private val isCollapsed: Boolean, - private val mergeId: String, - private val mergeData: List, - private val avatarRenderer: AvatarRenderer, - private val onCollapsedStateChanged: (Boolean) -> Unit -) : BaseEventItem() { + @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, + val avatarRenderer: AvatarRenderer, + val onCollapsedStateChanged: (Boolean) -> Unit + ) + class Holder : BaseHolder(STUB_ID) { val expandView by bind(R.id.itemMergedExpandTextView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index aad090db05..568347e83e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -32,47 +32,38 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle abstract class NoticeItem : BaseEventItem() { @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() { val readMarkerView by bind(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 }