diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 3b595077..38c436b5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -25,7 +25,7 @@ sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() object IsDisplayed : RoomDetailActions() - data class EventsDisplayed(val events: List) : RoomDetailActions() + data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index f2922d43..6af6c184 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -376,8 +376,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac homePermalinkHandler.launch(url) } - override fun onEventsVisible(events: List) { - roomDetailViewModel.process(RoomDetailActions.EventsDisplayed(events)) + override fun onEventVisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) } override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index da2754cf..d2662d79 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -44,7 +44,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId - private val displayedEventsObservable = BehaviorRelay.create() + private val displayedEventsObservable = BehaviorRelay.create() private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { @@ -69,11 +69,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> handleIsDisplayed() - is RoomDetailActions.SendMedia -> handleSendMedia(action) - is RoomDetailActions.EventsDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> handleIsDisplayed() + is RoomDetailActions.SendMedia -> handleSendMedia(action) + is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) + is RoomDetailActions.LoadMore -> handleLoadMore(action) } } @@ -196,7 +196,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.sendMedias(attachments) } - private fun handleEventDisplayed(action: RoomDetailActions.EventsDisplayed) { + private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { displayedEventsObservable.accept(action) } @@ -215,8 +215,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val mostRecentEvent = actions.map { it.events }.flatten().maxBy { it.displayIndex } - mostRecentEvent?.root?.eventId?.let { eventId -> + val mostRecentEvent = actions.maxBy { it.event.displayIndex } + mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } }) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 010ab48a..39240dce 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -33,18 +33,13 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged -import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent -import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextSameTypeEvents +import im.vector.riotredesign.features.home.room.detail.timeline.helper.* +import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ -import im.vector.riotredesign.features.home.room.detail.timeline.item.RoomMemberMergedItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer +import org.threeten.bp.LocalDateTime class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val timelineItemFactory: TimelineItemFactory, @@ -53,7 +48,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { interface Callback { - fun onEventsVisible(events: List) + fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) @@ -61,8 +56,10 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) } - + private val collapsedEventIds = linkedSetOf() + private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() + private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null @@ -91,16 +88,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized override fun onInserted(position: Int, count: Int) { assertUpdateCallbacksAllowed() - // When adding backwards we need to clear some events - if (position == modelCache.size) { - val previousCachedModel = modelCache.getOrNull(position - 1) - if (previousCachedModel != null) { - val numberOfMergedEvents = previousCachedModel.numberOfMergedEvents - for (i in 0..numberOfMergedEvents) { - modelCache[position - 1 - i] = null - } - } - } (0 until count).forEach { modelCache.add(position, null) } @@ -138,7 +125,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, .id("forward_loading_item") .addWhen(Timeline.Direction.FORWARDS) - val timelineModels = getModels() add(timelineModels) @@ -171,51 +157,90 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized private fun getModels(): List> { (0 until modelCache.size).forEach { position -> - if (modelCache[position] == null) { - buildAndCacheItemsAt(position) + // Should be build if not cached or if cached but contains mergedHeader or formattedDay + // We then are sure we always have items up to date. + if (modelCache[position] == null + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { + modelCache[position] = buildItemModels(position, currentSnapshot) } } return modelCache - .map { listOf(it?.eventModel, it?.formattedDayModel) } + .map { + val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) { + null + } else { + it.eventModel + } + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } .flatten() .filterNotNull() } - private fun buildAndCacheItemsAt(position: Int) { - val buildItemModelsResult = buildItemModels(position, currentSnapshot) - modelCache[position] = buildItemModelsResult - val prevResult = modelCache.getOrNull(position + 1) - if (prevResult != null && prevResult.eventModel is RoomMemberMergedItem && buildItemModelsResult.eventModel is RoomMemberMergedItem) { - buildItemModelsResult.eventModel.isCollapsed = prevResult.eventModel.isCollapsed - } - for (skipItemPosition in 0 until buildItemModelsResult.numberOfMergedEvents) { - val dumbModelsResult = CacheItemData(numberOfMergedEvents = buildItemModelsResult.numberOfMergedEvents) - modelCache[position + 1 + skipItemPosition] = dumbModelsResult - } - } private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] - val mergeableEvents = if (event.canBeMerged()) items.nextSameTypeEvents(currentPosition, minSize = 2) else emptyList() - val mergedEvents = listOf(event) + mergeableEvents - val nextDisplayableEvent = items.nextDisplayableEvent(currentPosition + mergeableEvents.size) - + val nextEvent = items.nextDisplayableEvent(currentPosition) val date = event.root.localDateTime() - val nextDate = nextDisplayableEvent?.root?.localDateTime() + val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val visibilityStateChangedListener = TimelineEventVisibilityStateChangedListener(callback, mergedEvents) - val epoxyModelId = mergedEvents.joinToString(separator = "_") { it.localId } - val eventModel = timelineItemFactory.create(event, mergeableEvents, nextDisplayableEvent, callback, visibilityStateChangedListener).also { - it.id(epoxyModelId) + val eventModel = timelineItemFactory.create(event, nextEvent, callback).also { + it.id(event.localId) + it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val daySeparatorItem = if (addDaySeparator) { + val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition) + val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) + + return CacheItemData(event.localId, eventModel, mergedHeaderModel, daySeparatorItem) + } + + private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { + return if (addDaySeparator) { val formattedDay = dateFormatter.formatMessageDay(date) DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) } else { null } - return CacheItemData(eventModel, daySeparatorItem, mergeableEvents.size) + } + + private fun buildMergedHeaderItem(event: TimelineEvent, + nextEvent: TimelineEvent?, + items: List, + addDaySeparator: Boolean, + currentPosition: Int): MergedHeaderItem? { + return if (!event.canBeMerged() || (nextEvent?.root?.type == event.root.type && !addDaySeparator)) { + null + } else { + val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) + if (prevSameTypeEvents.isEmpty()) { + null + } else { + val mergedEvents = (listOf(event) + prevSameTypeEvents) + val mergedData = mergedEvents.map { + val roomMember = event.roomMember + MergedHeaderItem.Data( + userId = event.root.sender ?: "", + avatarUrl = roomMember?.avatarUrl, + memberName = roomMember?.displayName ?: "", + eventId = it.localId + ) + } + val mergedEventIds = mergedEvents.map { it.localId } + val mergeId = mergedEventIds.joinToString(separator = "_") { it } + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { true } + if (isCollapsed) { + collapsedEventIds.addAll(mergedEventIds) + } else { + collapsedEventIds.removeAll(mergedEventIds) + } + MergedHeaderItem(isCollapsed, mergeId, mergedData) { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + } + } + } } private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { @@ -226,8 +251,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } private data class CacheItemData( + val localId: String, val eventModel: EpoxyModel<*>? = null, - val formattedDayModel: EpoxyModel<*>? = null, - val numberOfMergedEvents: Int = 0 + val mergedHeaderModel: MergedHeaderItem? = null, + val formattedDayModel: DaySeparatorItem? = null ) - diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index f980cb60..d5354434 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -16,14 +16,11 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.EmptyItem_ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener -import im.vector.riotredesign.features.home.room.detail.timeline.item.RoomMemberMergedItem class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val roomNameItemFactory: RoomNameItemFactory, @@ -34,57 +31,33 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val defaultItemFactory: DefaultItemFactory) { fun create(event: TimelineEvent, - mergeableEvents: List, nextEvent: TimelineEvent?, - callback: TimelineEventController.Callback?, - visibilityStateChangedListener: TimelineEventVisibilityStateChangedListener): EpoxyModelWithHolder<*> { + callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val computedModel = try { - if (mergeableEvents.isNotEmpty()) { - createMergedEvent(event, mergeableEvents, visibilityStateChangedListener) - } else { - when (event.root.type) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) - EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) - EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) - EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) - EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event) + when (event.root.type) { + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) + EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) + EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) + EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) + EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event) - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> callItemFactory.create(event) + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER -> callItemFactory.create(event) - EventType.ENCRYPTED, - EventType.ENCRYPTION, - EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER, - EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) + EventType.ENCRYPTED, + EventType.ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) - else -> null - } + else -> null } } catch (e: Exception) { defaultItemFactory.create(event, e) } - return (computedModel ?: EmptyItem_()).apply { - if (this is VectorEpoxyModel) { - this.setOnVisibilityStateChanged(visibilityStateChangedListener) - } - } - } - - private fun createMergedEvent(event: TimelineEvent, - mergeableEvents: List, - visibilityStateChangedListener: VectorEpoxyModel.OnVisibilityStateChangedListener): RoomMemberMergedItem { - - val events = listOf(event) + mergeableEvents - // We are reversing it as it does add items on a LinearLayout - val roomMemberItems = events.reversed().mapNotNull { - roomMemberItemFactory.create(it)?.apply { - id(it.localId) - } - } - return RoomMemberMergedItem(events, roomMemberItems, visibilityStateChangedListener) + return (computedModel ?: EmptyItem_()) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index deb4ad44..53d63f58 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.extensions.localDateTime object TimelineDisplayableEvents { @@ -58,11 +59,21 @@ fun List.nextSameTypeEvents(index: Int, minSize: Int): List.nextSameTypeEvents(index: Int, minSize: Int): List.prevSameTypeEvents(index: Int, minSize: Int): List { + val prevSub = subList(0, index + 1) + return prevSub + .reversed() + .nextSameTypeEvents(0, minSize) + .reversed() +} + fun List.nextDisplayableEvent(index: Int): TimelineEvent? { return if (index >= size - 1) { null diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index 0b329ec8..f103c18d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -24,12 +24,12 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val events: List) + private val event: TimelineEvent) : VectorEpoxyModel.OnVisibilityStateChangedListener { override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { - callback?.onEventsVisible(events) + callback?.onEventVisible(event) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt new file mode 100644 index 00000000..d5e05c11 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -0,0 +1,94 @@ +/* + * + * * 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.riotredesign.features.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.children +import im.vector.riotredesign.R +import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.features.home.AvatarRenderer + +data class MergedHeaderItem(private val isCollapsed: Boolean, + private val mergeId: String, + private val mergeData: List, + private val onCollapsedStateChanged: (Boolean) -> Unit +) : VectorEpoxyModel() { + + private val distinctMergeData = mergeData.distinctBy { it.userId } + + init { + id(mergeId) + } + + override fun getDefaultLayout(): Int { + return R.layout.item_timeline_event_merged_header + } + + override fun createNewHolder(): Holder { + return Holder() + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.expandView.setOnClickListener { + onCollapsedStateChanged(!isCollapsed) + } + if (isCollapsed) { + val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, mergeData.size, mergeData.size) + holder.summaryView.text = summary + holder.summaryView.visibility = View.VISIBLE + holder.avatarListView.visibility = View.VISIBLE + holder.avatarListView.children.forEachIndexed { index, view -> + val data = distinctMergeData.getOrNull(index) + if (data != null && view is ImageView) { + view.visibility = View.VISIBLE + AvatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view) + } else { + view.visibility = View.GONE + } + } + holder.separatorView.visibility = View.GONE + holder.expandView.setText(R.string.merged_events_expand) + } else { + holder.avatarListView.visibility = View.INVISIBLE + holder.summaryView.visibility = View.GONE + holder.separatorView.visibility = View.VISIBLE + holder.expandView.setText(R.string.merged_events_collapse) + } + } + + data class Data( + val eventId: String, + val userId: String, + val memberName: String, + val avatarUrl: String? + ) + + class Holder : VectorEpoxyHolder() { + val expandView by bind(R.id.itemMergedExpandTextView) + val summaryView by bind(R.id.itemMergedSummaryTextView) + val separatorView by bind(R.id.itemMergedSeparatorView) + val avatarListView by bind(R.id.itemMergedAvatarListView) + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt deleted file mode 100644 index 11f21745..00000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * - * * 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.riotredesign.features.home.room.detail.timeline.item - -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.children -import com.airbnb.epoxy.EpoxyModelGroup -import com.airbnb.epoxy.ModelGroupHolder -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.riotredesign.R -import im.vector.riotredesign.core.epoxy.VectorEpoxyModel -import im.vector.riotredesign.features.home.AvatarRenderer - -class RoomMemberMergedItem(val events: List, - private val roomMemberItems: List, - private val visibilityStateChangedListener: VectorEpoxyModel.OnVisibilityStateChangedListener -) : EpoxyModelGroup(R.layout.item_timeline_event_room_member_merged, roomMemberItems) { - - private val distinctRoomMemberItems = roomMemberItems.distinctBy { it.userId } - var isCollapsed = true - set(value) { - field = value - updateModelVisibility() - } - - init { - updateModelVisibility() - } - - override fun onVisibilityStateChanged(visibilityState: Int, view: ModelGroupHolder) { - super.onVisibilityStateChanged(visibilityState, view) - visibilityStateChangedListener.onVisibilityStateChanged(visibilityState) - } - - override fun bind(holder: ModelGroupHolder) { - super.bind(holder) - val expandView = holder.rootView.findViewById(R.id.itemMergedExpandTextView) - val summaryView = holder.rootView.findViewById(R.id.itemMergedSummaryTextView) - val separatorView = holder.rootView.findViewById(R.id.itemMergedSeparatorView) - val avatarListView = holder.rootView.findViewById(R.id.itemMergedAvatarListView) - if (isCollapsed) { - val summary = holder.rootView.resources.getQuantityString(R.plurals.membership_changes, roomMemberItems.size, roomMemberItems.size) - summaryView.text = summary - summaryView.visibility = View.VISIBLE - avatarListView.visibility = View.VISIBLE - avatarListView.children.forEachIndexed { index, view -> - val roomMemberItem = distinctRoomMemberItems.getOrNull(index) - if (roomMemberItem != null && view is ImageView) { - view.visibility = View.VISIBLE - AvatarRenderer.render(roomMemberItem.avatarUrl, roomMemberItem.userId, roomMemberItem.memberName?.toString(), view) - } else { - view.visibility = View.GONE - } - } - separatorView.visibility = View.GONE - expandView.setText(R.string.merged_events_expand) - } else { - avatarListView.visibility = View.INVISIBLE - summaryView.visibility = View.GONE - separatorView.visibility = View.VISIBLE - expandView.setText(R.string.merged_events_collapse) - } - expandView.setOnClickListener { _ -> - isCollapsed = !isCollapsed - updateModelVisibility() - bind(holder) - } - } - - private fun updateModelVisibility() { - roomMemberItems.forEach { - if (isCollapsed) { - it.hide() - } else { - it.show() - } - } - } - -} \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml b/vector/src/main/res/layout/item_timeline_event_merged_header.xml similarity index 76% rename from vector/src/main/res/layout/item_timeline_event_room_member_merged.xml rename to vector/src/main/res/layout/item_timeline_event_merged_header.xml index 2d33bcac..862a15db 100644 --- a/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_header.xml @@ -3,6 +3,8 @@ @@ -11,8 +13,8 @@ layout="@layout/vector_message_merge_avatar_list" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="80dp" - android:layout_marginLeft="80dp" + android:layout_marginStart="64dp" + android:layout_marginLeft="64dp" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" @@ -25,18 +27,17 @@ android:id="@+id/itemMergedExpandTextView" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" + android:paddingRight="8dp" android:layout_marginTop="2dp" - android:layout_marginEnd="24dp" - android:layout_marginRight="24dp" - android:layout_marginBottom="8dp" + android:paddingLeft="8dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" android:text="@string/merged_events_expand" android:textColor="?attr/colorAccent" android:textSize="14sp" android:textStyle="italic" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@id/itemMergedAvatarListView" - android:layout_marginLeft="8dp" /> + app:layout_constraintTop_toTopOf="parent" /> - - \ No newline at end of file