diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index a80e92a0..c43ff438 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor @@ -57,7 +57,7 @@ internal class TimelineTest : InstrumentedTest { val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) - val roomMemberExtractor = RoomMemberExtractor(ROOM_ID) + val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ca971b65..58044278 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -29,7 +29,8 @@ data class TimelineEvent( val root: Event, val localId: String, val displayIndex: Int, - val roomMember: RoomMember?, + val senderName: String?, + val senderAvatar: String?, val sendState: SendState ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 6df95e10..28470455 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -56,14 +56,10 @@ internal fun ChunkEntity.merge(roomId: String, if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken this.isLastForward = chunkToMerge.isLastForward - this.forwardsStateIndex = chunkToMerge.forwardsStateIndex - this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) } else { this.prevToken = chunkToMerge.prevToken this.isLastBackward = chunkToMerge.isLastBackward - this.backwardsStateIndex = chunkToMerge.backwardsStateIndex - this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } eventsToMerge.forEach { @@ -119,20 +115,20 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex this.sendState = SendState.SYNCED } - // We are not using the order of the list, but will be sorting with displayIndex field - events.add(eventEntity) + val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size + events.add(position, eventEntity) } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 146794e1..24e51355 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.session.room.invite.InviteTask import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.send.DefaultSendService @@ -45,7 +45,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, private val taskExecutor: TaskExecutor) { fun instantiate(roomId: String): Room { - val roomMemberExtractor = RoomMemberExtractor(roomId) + val roomMemberExtractor = SenderRoomMemberExtractor(roomId) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt index 6c6d78ed..40266cb7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt @@ -48,6 +48,18 @@ internal class RoomMembers(private val realm: Realm, } } + fun isUniqueDisplayName(displayName: String?): Boolean { + if(displayName.isNullOrEmpty()){ + return true + } + return EventEntity + .where(realm, roomId, EventType.STATE_ROOM_MEMBER) + .contains(EventEntityFields.CONTENT, displayName) + .distinct(EventEntityFields.STATE_KEY) + .findAll() + .size == 1 + } + fun queryRoomMembersEvent(): RealmQuery { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt similarity index 56% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt index f2ec6a05..d07175d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt @@ -20,48 +20,45 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.next import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import io.realm.Realm +import io.realm.RealmList import io.realm.RealmQuery -internal class RoomMemberExtractor(private val roomId: String) { - - private val cached = HashMap() +internal class SenderRoomMemberExtractor(private val roomId: String) { fun extractFrom(event: EventEntity): RoomMember? { val sender = event.sender ?: return null - val cacheKey = sender + event.stateIndex - if (cached.containsKey(cacheKey)) { - return cached[cacheKey] - } // If the event is unlinked we want to fetch unlinked state events val unlinked = event.isUnlinked - // When stateIndex is negative, we try to get the next stateEvent prevContent() - // If prevContent is null we fallback to the Int.MIN state events content() - val content = if (event.stateIndex <= 0) { - baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent - ?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content - } else { - baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content + val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null + val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId) + val content = when { + chunkEntity == null -> null + event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent + else -> baseQuery(chunkEntity.events, sender, unlinked).prev(since = event.stateIndex)?.content } - val roomMember: RoomMember? = ContentMapper.map(content).toModel() - cached[cacheKey] = roomMember - return roomMember + + val fallbackContent = content + ?: baseQuery(roomEntity.untimelinedStateEvents, sender, unlinked).prev(since = event.stateIndex)?.content + + return ContentMapper.map(fallbackContent).toModel() } - private fun baseQuery(realm: Realm, - roomId: String, + private fun baseQuery(list: RealmList, sender: String, isUnlinked: Boolean): RealmQuery { - - val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY - return EventEntity - .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode) + return list + .where() .equalTo(EventEntityFields.STATE_KEY, sender) + .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .equalTo(EventEntityFields.IS_UNLINKED, isUnlinked) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 89a8307b..58274288 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,14 +27,24 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer -import io.realm.* +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -87,9 +97,16 @@ internal class DefaultTimeline( private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) { handleInitialLoad() } else { + // If changeSet has deletion we are having a gap, so we clear everything + if(changeSet.deletionRanges.isNotEmpty()){ + prevDisplayIndex = DISPLAY_INDEX_UNKNOWN + nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + builtEvents.clear() + timelineEventFactory.clear() + } changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) @@ -108,6 +125,7 @@ internal class DefaultTimeline( buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) postSnapshot() } + } } } @@ -298,9 +316,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index 20c0ebee..28cd2a9d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -19,19 +19,36 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor -internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) { +internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) { + + private val cached = mutableMapOf() fun create(eventEntity: EventEntity): TimelineEvent { - val roomMember = roomMemberExtractor.extractFrom(eventEntity) + val sender = eventEntity.sender + val cacheKey = sender + eventEntity.stateIndex + val senderData = cached.getOrPut(cacheKey) { + val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity) + SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) + } return TimelineEvent( eventEntity.asDomain(), eventEntity.localId, eventEntity.displayIndex, - roomMember, + senderData.senderName, + senderData.senderAvatar, eventEntity.sendState ) } + fun clear(){ + cached.clear() + } + + private data class SenderData( + val senderName: String?, + val senderAvatar: String? + ) + } \ No newline at end of file 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 39240dce..05b20b2f 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 @@ -24,6 +24,8 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMember 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 @@ -33,7 +35,15 @@ 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.* +import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper +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.prevSameTypeEvents 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.MergedHeaderItem @@ -160,8 +170,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, // 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]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -217,24 +227,32 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, if (prevSameTypeEvents.isEmpty()) { null } else { - val mergedEvents = (listOf(event) + prevSameTypeEvents) - val mergedData = mergedEvents.map { - val roomMember = event.roomMember + val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() + val mergedData = mergedEvents.map { mergedEvent -> + val eventContent: RoomMember? = mergedEvent.root.content.toModel() + val prevEventContent: RoomMember? = mergedEvent.root.prevContent.toModel() + val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, mergedEvent) + val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, mergedEvent) MergedHeaderItem.Data( - userId = event.root.sender ?: "", - avatarUrl = roomMember?.avatarUrl, - memberName = roomMember?.displayName ?: "", - eventId = it.localId + userId = mergedEvent.root.sender ?: "", + avatarUrl = senderAvatar, + memberName = senderName ?: "", + eventId = mergedEvent.localId ) } val mergedEventIds = mergedEvents.map { it.localId } - val mergeId = mergedEventIds.joinToString(separator = "_") { it } - val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { true } + // We try to find if one of the item id were used as mergeItemCollapseStates key + // => handle case where paginating from mergeable events and we get more + val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) } else { collapsedEventIds.removeAll(mergedEventIds) } + val mergeId = mergedEventIds.joinToString(separator = "_") { it } MergedHeaderItem(isCollapsed, mergeId, mergedData) { mergeItemCollapseStates[event.localId] = it requestModelBuild() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt index e83183ef..42f6ba0e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -30,27 +30,26 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class CallItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val text = buildNoticeText(event.root, roomMember) ?: return null + val text = buildNoticeText(event.root, event.senderName) ?: return null return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } - private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? { + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { return when { EventType.CALL_INVITE == event.type -> { val content = event.content.toModel() ?: return null val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO return if (isVideoCall) { - stringProvider.getString(R.string.notice_placed_video_call, roomMember.displayName) + stringProvider.getString(R.string.notice_placed_video_call, senderName) } else { - stringProvider.getString(R.string.notice_placed_voice_call, roomMember.displayName) + stringProvider.getString(R.string.notice_placed_voice_call, senderName) } } - EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, roomMember.displayName) - EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, roomMember.displayName) + EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName) + EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName) else -> null } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 73be1cf2..f17cc295 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -65,8 +65,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ): VectorEpoxyModel<*>? { val eventId = event.root.eventId ?: return null - val roomMember = event.roomMember - val nextRoomMember = nextEvent?.roomMember val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() @@ -75,14 +73,15 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ?: false val showInformation = addDaySeparator - || nextRoomMember != roomMember + || event.senderAvatar != nextEvent?.senderAvatar + || event.senderName != nextEvent?.senderName || nextEvent?.root?.type != EventType.MESSAGE || isNextMessageReceivedMoreThanOneHourAgo val messageContent: MessageContent = event.root.content.toModel() ?: return null val time = timelineDateFormatter.formatMessageHour(date) - val avatarUrl = roomMember?.avatarUrl - val memberName = roomMember?.displayName ?: event.root.sender ?: "" + val avatarUrl = event.senderAvatar + val memberName = event.senderName ?: event.root.sender ?: "" val formattedMemberName = span(memberName) { textColor = colorProvider.getColor(getColorFor(event.root.sender ?: "")) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt index cd87e7bd..3a3a91f1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt @@ -31,15 +31,14 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val noticeText = buildNoticeText(event.root, roomMember) ?: return null + val noticeText = buildNoticeText(event.root, event.senderName) ?: return null return NoticeItem_() .noticeText(noticeText) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } - private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? { + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { val content = event.content.toModel() ?: return null val formattedVisibility = when (content.historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -47,7 +46,7 @@ class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvide RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) } - return stringProvider.getString(R.string.notice_made_future_room_visibility, roomMember.displayName, formattedVisibility) + return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt index 5d2efcba..77e5920c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ @@ -31,18 +32,20 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class RoomMemberItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val noticeText = buildRoomMemberNotice(event) ?: return null + val eventContent: RoomMember? = event.root.content.toModel() + val prevEventContent: RoomMember? = event.root.prevContent.toModel() + val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null + val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event) + val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event) + return NoticeItem_() .userId(event.root.sender ?: "") .noticeText(noticeText) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(senderAvatar) + .memberName(senderName) } - private fun buildRoomMemberNotice(event: TimelineEvent): String? { - val eventContent: RoomMember? = event.root.content.toModel() - val prevEventContent: RoomMember? = event.root.prevContent.toModel() + private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { val isMembershipEvent = prevEventContent?.membership != eventContent?.membership return if (isMembershipEvent) { buildMembershipNotice(event, eventContent, prevEventContent) @@ -62,7 +65,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName) else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.root.sender, prevEventContent?.displayName, eventContent?.displayName) + event.root.sender, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -72,7 +75,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { displayText.append(" ") stringProvider.getString(R.string.notice_avatar_changed_too) } else { - stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName) + stringProvider.getString(R.string.notice_avatar_url_changed, event.senderName) } displayText.append(displayAvatarText) } @@ -80,16 +83,16 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { } private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { - val senderDisplayName = event.roomMember?.displayName ?: return null + val senderDisplayName = event.senderName ?: event.root.sender val targetDisplayName = eventContent?.displayName ?: event.root.sender return when { Membership.INVITE == eventContent?.membership -> { // TODO get userId - val selfUserId: String = "" + val selfUserId = "" when { eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, - targetDisplayName, eventContent.thirdPartyInvite?.displayName) + targetDisplayName, eventContent.thirdPartyInvite?.displayName) TextUtils.equals(event.root.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) event.root.stateKey.isNullOrEmpty() -> @@ -106,7 +109,8 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { if (prevEventContent?.membership == Membership.INVITE) { stringProvider.getString(R.string.notice_room_reject, senderDisplayName) } else { - stringProvider.getString(R.string.notice_room_leave, senderDisplayName) + val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event) + stringProvider.getString(R.string.notice_room_leave, leftDisplayName) } } else if (prevEventContent?.membership == Membership.INVITE) { stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt index 3904d7c7..be33c44e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt @@ -29,20 +29,16 @@ class RoomNameItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val content: RoomNameContent? = event.root.content.toModel() - val roomMember = event.roomMember - if (content == null || roomMember == null) { - return null - } + val content: RoomNameContent = event.root.content.toModel() ?: return null val text = if (!TextUtils.isEmpty(content.name)) { - stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name) + stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name) } else { - stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName) + stringProvider.getString(R.string.notice_room_name_removed, event.senderName) } return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt index 5aa31c9b..34e55897 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt @@ -28,20 +28,16 @@ class RoomTopicItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val content: RoomTopicContent? = event.root.content.toModel() - val roomMember = event.roomMember - if (content == null || roomMember == null) { - return null - } + val content: RoomTopicContent = event.root.content.toModel() ?: return null val text = if (content.topic.isNullOrEmpty()) { - stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName) + stringProvider.getString(R.string.notice_room_topic_removed, event.senderName) } else { - stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic) + stringProvider.getString(R.string.notice_room_topic_changed, event.senderName, content.topic) } return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt index c88bd7e9..d078e72c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt @@ -34,28 +34,18 @@ class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutM // This happens many times a second during a scroll, so be wary of the code you place here. // We are given a few useful parameters to help us work out if we need to load some more data, // but first we check if we are waiting for the previous load to finish. + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val totalItemCount = layoutManager.itemCount - // The minimum amount of items to have below your current scroll position - // before loading more. - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - previousTotalItemCount = totalItemCount - if (totalItemCount == 0) { - loadingForwards = true - loadingBackwards = true - } - } - // If it’s still loading, we check to see if the dataset count has + // We check to see if the dataset count has // changed, if so we conclude it has finished loading - if (totalItemCount > previousTotalItemCount) { + if (totalItemCount != previousTotalItemCount) { + previousTotalItemCount = totalItemCount loadingBackwards = false loadingForwards = false - previousTotalItemCount = totalItemCount } // If it isn’t currently loading, we check to see if we have reached // the visibleThreshold and need to reload more data. diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt new file mode 100644 index 00000000..499f92f7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt @@ -0,0 +1,40 @@ +/* + * 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 im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent + +object RoomMemberEventHelper { + + fun senderAvatar(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? { + return if (eventContent?.membership == Membership.LEAVE && eventContent.avatarUrl == null && prevEventContent?.avatarUrl != null) { + prevEventContent.avatarUrl + } else { + event.senderAvatar + } + } + + fun senderName(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? { + return if (eventContent?.membership == Membership.LEAVE && eventContent.displayName == null && prevEventContent?.displayName != null) { + prevEventContent.displayName + } else { + event.senderName + } + } +} \ No newline at end of file