forked from GitHub-Mirror/riotX-android
Timeline : merged events are now handled directly within the recyclerview and do not need a LinearLayout.
This commit is contained in:
parent
b3e2eca43d
commit
287feace12
@ -25,7 +25,7 @@ sealed class RoomDetailActions {
|
|||||||
data class SendMessage(val text: String) : RoomDetailActions()
|
data class SendMessage(val text: String) : RoomDetailActions()
|
||||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||||
object IsDisplayed : RoomDetailActions()
|
object IsDisplayed : RoomDetailActions()
|
||||||
data class EventsDisplayed(val events: List<TimelineEvent>) : RoomDetailActions()
|
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||||
|
|
||||||
}
|
}
|
@ -376,8 +376,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
homePermalinkHandler.launch(url)
|
homePermalinkHandler.launch(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventsVisible(events: List<TimelineEvent>) {
|
override fun onEventVisible(event: TimelineEvent) {
|
||||||
roomDetailViewModel.process(RoomDetailActions.EventsDisplayed(events))
|
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
|
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 room = session.getRoom(initialState.roomId)!!
|
||||||
private val roomId = initialState.roomId
|
private val roomId = initialState.roomId
|
||||||
private val eventId = initialState.eventId
|
private val eventId = initialState.eventId
|
||||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventsDisplayed>()
|
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||||
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
|
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||||
@ -69,11 +69,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
|
|
||||||
fun process(action: RoomDetailActions) {
|
fun process(action: RoomDetailActions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||||
is RoomDetailActions.SendMedia -> handleSendMedia(action)
|
is RoomDetailActions.SendMedia -> handleSendMedia(action)
|
||||||
is RoomDetailActions.EventsDisplayed -> handleEventDisplayed(action)
|
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
room.sendMedias(attachments)
|
room.sendMedias(attachments)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEventDisplayed(action: RoomDetailActions.EventsDisplayed) {
|
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
|
||||||
displayedEventsObservable.accept(action)
|
displayedEventsObservable.accept(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,8 +215,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||||||
.buffer(1, TimeUnit.SECONDS)
|
.buffer(1, TimeUnit.SECONDS)
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.subscribeBy(onNext = { actions ->
|
.subscribeBy(onNext = { actions ->
|
||||||
val mostRecentEvent = actions.map { it.events }.flatten().maxBy { it.displayIndex }
|
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
||||||
mostRecentEvent?.root?.eventId?.let { eventId ->
|
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
|
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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.epoxy.LoadingItemModel_
|
||||||
import im.vector.riotredesign.core.extensions.localDateTime
|
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.factory.TimelineItemFactory
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||||
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.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.ImageContentRenderer
|
||||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||||
|
import org.threeten.bp.LocalDateTime
|
||||||
|
|
||||||
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||||
private val timelineItemFactory: TimelineItemFactory,
|
private val timelineItemFactory: TimelineItemFactory,
|
||||||
@ -53,7 +48,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onEventsVisible(events: List<TimelineEvent>)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
fun onUrlClicked(url: String)
|
fun onUrlClicked(url: String)
|
||||||
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
|
||||||
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.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)
|
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val collapsedEventIds = linkedSetOf<String>()
|
||||||
|
private val mergeItemCollapseStates = HashMap<String, Boolean>()
|
||||||
private val modelCache = arrayListOf<CacheItemData?>()
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
|
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
private var inSubmitList: Boolean = false
|
private var inSubmitList: Boolean = false
|
||||||
private var timeline: Timeline? = null
|
private var timeline: Timeline? = null
|
||||||
@ -91,16 +88,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
assertUpdateCallbacksAllowed()
|
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 {
|
(0 until count).forEach {
|
||||||
modelCache.add(position, null)
|
modelCache.add(position, null)
|
||||||
}
|
}
|
||||||
@ -138,7 +125,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
.id("forward_loading_item")
|
.id("forward_loading_item")
|
||||||
.addWhen(Timeline.Direction.FORWARDS)
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
|
|
||||||
val timelineModels = getModels()
|
val timelineModels = getModels()
|
||||||
add(timelineModels)
|
add(timelineModels)
|
||||||
|
|
||||||
@ -171,51 +157,90 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun getModels(): List<EpoxyModel<*>> {
|
private fun getModels(): List<EpoxyModel<*>> {
|
||||||
(0 until modelCache.size).forEach { position ->
|
(0 until modelCache.size).forEach { position ->
|
||||||
if (modelCache[position] == null) {
|
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||||
buildAndCacheItemsAt(position)
|
// 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
|
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()
|
.flatten()
|
||||||
.filterNotNull()
|
.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<TimelineEvent>): CacheItemData {
|
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||||
val event = items[currentPosition]
|
val event = items[currentPosition]
|
||||||
val mergeableEvents = if (event.canBeMerged()) items.nextSameTypeEvents(currentPosition, minSize = 2) else emptyList()
|
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
||||||
val mergedEvents = listOf(event) + mergeableEvents
|
|
||||||
val nextDisplayableEvent = items.nextDisplayableEvent(currentPosition + mergeableEvents.size)
|
|
||||||
|
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
val nextDate = nextDisplayableEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
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 {
|
val eventModel = timelineItemFactory.create(event, nextEvent, callback).also {
|
||||||
it.id(epoxyModelId)
|
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)
|
val formattedDay = dateFormatter.formatMessageDay(date)
|
||||||
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
return CacheItemData(eventModel, daySeparatorItem, mergeableEvents.size)
|
}
|
||||||
|
|
||||||
|
private fun buildMergedHeaderItem(event: TimelineEvent,
|
||||||
|
nextEvent: TimelineEvent?,
|
||||||
|
items: List<TimelineEvent>,
|
||||||
|
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) {
|
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||||
@ -226,8 +251,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private data class CacheItemData(
|
private data class CacheItemData(
|
||||||
|
val localId: String,
|
||||||
val eventModel: EpoxyModel<*>? = null,
|
val eventModel: EpoxyModel<*>? = null,
|
||||||
val formattedDayModel: EpoxyModel<*>? = null,
|
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||||
val numberOfMergedEvents: Int = 0
|
val formattedDayModel: DaySeparatorItem? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,14 +16,11 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
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.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.core.epoxy.EmptyItem_
|
import im.vector.riotredesign.core.epoxy.EmptyItem_
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
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.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,
|
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
||||||
private val roomNameItemFactory: RoomNameItemFactory,
|
private val roomNameItemFactory: RoomNameItemFactory,
|
||||||
@ -34,57 +31,33 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||||||
private val defaultItemFactory: DefaultItemFactory) {
|
private val defaultItemFactory: DefaultItemFactory) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
mergeableEvents: List<TimelineEvent>,
|
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||||
visibilityStateChangedListener: TimelineEventVisibilityStateChangedListener): EpoxyModelWithHolder<*> {
|
|
||||||
|
|
||||||
val computedModel = try {
|
val computedModel = try {
|
||||||
if (mergeableEvents.isNotEmpty()) {
|
when (event.root.type) {
|
||||||
createMergedEvent(event, mergeableEvents, visibilityStateChangedListener)
|
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
||||||
} else {
|
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
|
||||||
when (event.root.type) {
|
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
|
||||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
|
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
|
||||||
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
|
EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.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_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> callItemFactory.create(event)
|
EventType.CALL_ANSWER -> callItemFactory.create(event)
|
||||||
|
|
||||||
EventType.ENCRYPTED,
|
EventType.ENCRYPTED,
|
||||||
EventType.ENCRYPTION,
|
EventType.ENCRYPTION,
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
defaultItemFactory.create(event, e)
|
defaultItemFactory.create(event, e)
|
||||||
}
|
}
|
||||||
return (computedModel ?: EmptyItem_()).apply {
|
return (computedModel ?: EmptyItem_())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
|
|
||||||
object TimelineDisplayableEvents {
|
object TimelineDisplayableEvents {
|
||||||
|
|
||||||
@ -58,11 +59,21 @@ fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<Timel
|
|||||||
}
|
}
|
||||||
val timelineEvent = this[index]
|
val timelineEvent = this[index]
|
||||||
val nextSubList = subList(index + 1, size)
|
val nextSubList = subList(index + 1, size)
|
||||||
val indexOfFirstDifferentEventType = nextSubList.indexOfFirst { it.root.type != timelineEvent.root.type }
|
val indexOfNextDay = nextSubList.indexOfFirst {
|
||||||
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
|
val date = it.root.localDateTime()
|
||||||
|
val nextDate = timelineEvent.root.localDateTime()
|
||||||
|
date.toLocalDate() != nextDate.toLocalDate()
|
||||||
|
}
|
||||||
|
val nextSameDayEvents = if (indexOfNextDay == -1) {
|
||||||
nextSubList
|
nextSubList
|
||||||
} else {
|
} else {
|
||||||
nextSubList.subList(0, indexOfFirstDifferentEventType)
|
nextSubList.subList(0, indexOfNextDay)
|
||||||
|
}
|
||||||
|
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.type != timelineEvent.root.type }
|
||||||
|
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
|
||||||
|
nextSameDayEvents
|
||||||
|
} else {
|
||||||
|
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
|
||||||
}
|
}
|
||||||
if (sameTypeEvents.size < minSize) {
|
if (sameTypeEvents.size < minSize) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
@ -70,6 +81,14 @@ fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<Timel
|
|||||||
return sameTypeEvents
|
return sameTypeEvents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun List<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||||
|
val prevSub = subList(0, index + 1)
|
||||||
|
return prevSub
|
||||||
|
.reversed()
|
||||||
|
.nextSameTypeEvents(0, minSize)
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||||
return if (index >= size - 1) {
|
return if (index >= size - 1) {
|
||||||
null
|
null
|
||||||
|
@ -24,12 +24,12 @@ 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.TimelineEventController
|
||||||
|
|
||||||
class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||||
private val events: List<TimelineEvent>)
|
private val event: TimelineEvent)
|
||||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||||
|
|
||||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||||
if (visibilityState == VisibilityState.VISIBLE) {
|
if (visibilityState == VisibilityState.VISIBLE) {
|
||||||
callback?.onEventsVisible(events)
|
callback?.onEventVisible(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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<Data>,
|
||||||
|
private val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
|
) : VectorEpoxyModel<MergedHeaderItem.Holder>() {
|
||||||
|
|
||||||
|
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<TextView>(R.id.itemMergedExpandTextView)
|
||||||
|
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
||||||
|
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
|
||||||
|
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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<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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -3,6 +3,8 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
@ -11,8 +13,8 @@
|
|||||||
layout="@layout/vector_message_merge_avatar_list"
|
layout="@layout/vector_message_merge_avatar_list"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="80dp"
|
android:layout_marginStart="64dp"
|
||||||
android:layout_marginLeft="80dp"
|
android:layout_marginLeft="64dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginRight="16dp"
|
android:layout_marginRight="16dp"
|
||||||
@ -25,18 +27,17 @@
|
|||||||
android:id="@+id/itemMergedExpandTextView"
|
android:id="@+id/itemMergedExpandTextView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:paddingRight="8dp"
|
||||||
android:layout_marginTop="2dp"
|
android:layout_marginTop="2dp"
|
||||||
android:layout_marginEnd="24dp"
|
android:paddingLeft="8dp"
|
||||||
android:layout_marginRight="24dp"
|
android:paddingTop="4dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:paddingBottom="4dp"
|
||||||
android:text="@string/merged_events_expand"
|
android:text="@string/merged_events_expand"
|
||||||
android:textColor="?attr/colorAccent"
|
android:textColor="?attr/colorAccent"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="italic"
|
android:textStyle="italic"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/itemMergedAvatarListView"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
android:layout_marginLeft="8dp" />
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/itemMergedSeparatorView"
|
android:id="@+id/itemMergedSeparatorView"
|
||||||
@ -61,13 +62,4 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/itemMergedSeparatorView"
|
app:layout_constraintTop_toBottomOf="@id/itemMergedSeparatorView"
|
||||||
tools:text="3 membership changes" />
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue
Block a user