Timeline : start to handle merging room member events. Need to get better perf.
This commit is contained in:
parent
dab80466c5
commit
b3e2eca43d
|
@ -25,7 +25,7 @@ sealed class RoomDetailActions {
|
|||
data class SendMessage(val text: String) : RoomDetailActions()
|
||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||
object IsDisplayed : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class EventsDisplayed(val events: List<TimelineEvent>) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
|
||||
}
|
|
@ -50,12 +50,7 @@ import im.vector.riotredesign.core.extensions.observeEvent
|
|||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotredesign.core.platform.VectorBaseFragment
|
||||
import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
|
||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
|
||||
import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
|
||||
import im.vector.riotredesign.core.utils.checkPermissions
|
||||
import im.vector.riotredesign.core.utils.openCamera
|
||||
import im.vector.riotredesign.core.utils.*
|
||||
import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter
|
||||
|
@ -381,8 +376,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||
homePermalinkHandler.launch(url)
|
||||
}
|
||||
|
||||
override fun onEventVisible(event: TimelineEvent) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||
override fun onEventsVisible(events: List<TimelineEvent>) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventsDisplayed(events))
|
||||
}
|
||||
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||
|
|
|
@ -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<RoomDetailActions.EventDisplayed>()
|
||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventsDisplayed>()
|
||||
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||
|
@ -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.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,7 +196,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
room.sendMedias(attachments)
|
||||
}
|
||||
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||
private fun handleEventDisplayed(action: RoomDetailActions.EventsDisplayed) {
|
||||
displayedEventsObservable.accept(action)
|
||||
}
|
||||
|
||||
|
@ -215,8 +215,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribeBy(onNext = { actions ->
|
||||
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||
val mostRecentEvent = actions.map { it.events }.flatten().maxBy { it.displayIndex }
|
||||
mostRecentEvent?.root?.eventId?.let { eventId ->
|
||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -24,7 +24,6 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||
|
@ -32,15 +31,18 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVideoConte
|
|||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||
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.item.DaySeparatorItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.RoomMemberMergedItem
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
|
||||
|
@ -51,7 +53,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||
|
||||
interface Callback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onEventsVisible(events: List<TimelineEvent>)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
|
||||
|
@ -59,7 +61,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
||||
}
|
||||
|
||||
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var timeline: Timeline? = null
|
||||
|
@ -72,7 +75,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(position until (position + count)).forEach {
|
||||
modelCache[it] = emptyList()
|
||||
modelCache[it] = null
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
@ -88,11 +91,18 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
@Synchronized
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
||||
modelCache[position - 1] = emptyList()
|
||||
// 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, emptyList())
|
||||
modelCache.add(position, null)
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
@ -161,53 +171,63 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||
@Synchronized
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
if (modelCache[position].isEmpty()) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
if (modelCache[position] == null) {
|
||||
buildAndCacheItemsAt(position)
|
||||
}
|
||||
}
|
||||
return modelCache.flatten()
|
||||
return modelCache
|
||||
.map { listOf(it?.eventModel, it?.formattedDayModel) }
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||
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<TimelineEvent>): CacheItemData {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextDisplayableEvent(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 date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val nextDate = nextDisplayableEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val visibilityStateChangedListener = TimelineEventVisibilityStateChangedListener(callback, mergedEvents)
|
||||
val epoxyModelId = mergedEvents.joinToString(separator = "_") { it.localId }
|
||||
|
||||
timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
epoxyModels.add(it)
|
||||
val eventModel = timelineItemFactory.create(event, mergeableEvents, nextDisplayableEvent, callback, visibilityStateChangedListener).also {
|
||||
it.id(epoxyModelId)
|
||||
}
|
||||
if (addDaySeparator) {
|
||||
val daySeparatorItem = if (addDaySeparator) {
|
||||
val formattedDay = dateFormatter.formatMessageDay(date)
|
||||
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||
epoxyModels.add(daySeparatorItem)
|
||||
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return epoxyModels
|
||||
return CacheItemData(eventModel, daySeparatorItem, mergeableEvents.size)
|
||||
}
|
||||
|
||||
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||
val shouldAdd = timeline?.let {
|
||||
it.hasMoreToLoad(direction)
|
||||
} ?: false
|
||||
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
|
||||
addIf(shouldAdd, this@TimelineEventController)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||
private val event: TimelineEvent)
|
||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||
private data class CacheItemData(
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val formattedDayModel: EpoxyModel<*>? = null,
|
||||
val numberOfMergedEvents: Int = 0
|
||||
)
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventVisible(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -34,6 +34,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||
val roomMember = event.roomMember ?: return null
|
||||
val noticeText = buildRoomMemberNotice(event) ?: return null
|
||||
return NoticeItem_()
|
||||
.userId(event.root.sender ?: "")
|
||||
.noticeText(noticeText)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
|
@ -57,9 +58,9 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||
val displayNameText = when {
|
||||
prevEventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
|
||||
eventContent?.displayName.isNullOrEmpty() ->
|
||||
eventContent?.displayName.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
|
||||
else ->
|
||||
else ->
|
||||
stringProvider.getString(R.string.notice_display_name_changed_from,
|
||||
event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
|
||||
}
|
||||
|
@ -86,20 +87,20 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||
// TODO get userId
|
||||
val selfUserId: String = ""
|
||||
when {
|
||||
eventContent.thirdPartyInvite != null ->
|
||||
eventContent.thirdPartyInvite != null ->
|
||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
|
||||
TextUtils.equals(event.root.stateKey, selfUserId) ->
|
||||
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
|
||||
event.root.stateKey.isNullOrEmpty() ->
|
||||
event.root.stateKey.isNullOrEmpty() ->
|
||||
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
|
||||
else ->
|
||||
else ->
|
||||
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
|
||||
}
|
||||
}
|
||||
Membership.JOIN == eventContent?.membership ->
|
||||
Membership.JOIN == eventContent?.membership ->
|
||||
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
|
||||
Membership.LEAVE == eventContent?.membership ->
|
||||
Membership.LEAVE == eventContent?.membership ->
|
||||
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
|
||||
return if (TextUtils.equals(event.root.sender, event.root.stateKey)) {
|
||||
if (prevEventContent?.membership == Membership.INVITE) {
|
||||
|
@ -116,11 +117,11 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||
} else {
|
||||
null
|
||||
}
|
||||
Membership.BAN == eventContent?.membership ->
|
||||
Membership.BAN == eventContent?.membership ->
|
||||
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
|
||||
Membership.KNOCK == eventContent?.membership ->
|
||||
Membership.KNOCK == eventContent?.membership ->
|
||||
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
|
||||
else -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,11 +16,14 @@
|
|||
|
||||
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,
|
||||
|
@ -31,33 +34,57 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||
private val defaultItemFactory: DefaultItemFactory) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
mergeableEvents: List<TimelineEvent>,
|
||||
nextEvent: TimelineEvent?,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||
callback: TimelineEventController.Callback?,
|
||||
visibilityStateChangedListener: TimelineEventVisibilityStateChangedListener): EpoxyModelWithHolder<*> {
|
||||
|
||||
val computedModel = try {
|
||||
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)
|
||||
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)
|
||||
|
||||
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_()
|
||||
return (computedModel ?: EmptyItem_()).apply {
|
||||
if (this is VectorEpoxyModel) {
|
||||
this.setOnVisibilityStateChanged(visibilityStateChangedListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMergedEvent(event: TimelineEvent,
|
||||
mergeableEvents: List<TimelineEvent>,
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
|
@ -48,8 +48,30 @@ fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
|||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
return root.type == EventType.STATE_ROOM_MEMBER
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||
if (index >= size - 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val timelineEvent = this[index]
|
||||
val nextSubList = subList(index + 1, size)
|
||||
val indexOfFirstDifferentEventType = nextSubList.indexOfFirst { it.root.type != timelineEvent.root.type }
|
||||
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
|
||||
nextSubList
|
||||
} else {
|
||||
nextSubList.subList(0, indexOfFirstDifferentEventType)
|
||||
}
|
||||
if (sameTypeEvents.size < minSize) {
|
||||
return emptyList()
|
||||
}
|
||||
return sameTypeEvents
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||
return if (index == size - 1) {
|
||||
return if (index >= size - 1) {
|
||||
null
|
||||
} else {
|
||||
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
*
|
||||
* * 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.helper
|
||||
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
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<TimelineEvent>)
|
||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventsVisible(events)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
*
|
||||
* * 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<TimelineEvent>,
|
||||
private val roomMemberItems: List<NoticeItem>,
|
||||
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<TextView>(R.id.itemMergedExpandTextView)
|
||||
val summaryView = holder.rootView.findViewById<TextView>(R.id.itemMergedSummaryTextView)
|
||||
val separatorView = holder.rootView.findViewById<View>(R.id.itemMergedSeparatorView)
|
||||
val avatarListView = holder.rootView.findViewById<ViewGroup>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/itemMergedAvatarListView"
|
||||
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_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemMergedExpandTextView"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemMergedExpandTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
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" />
|
||||
|
||||
<View
|
||||
android:id="@+id/itemMergedSeparatorView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="@id/itemMergedExpandTextView"
|
||||
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemMergedExpandTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemMergedSummaryTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemMergedSeparatorView"
|
||||
tools:text="3 membership changes" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/epoxy_model_group_child_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemMergedSummaryTextView" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_1"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_2"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_3"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_4"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_5"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</LinearLayout>
|
Loading…
Reference in New Issue