From 51a4c936761b5f5194832859bd3e4f9e74a4c72c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Aug 2019 19:12:22 +0200 Subject: [PATCH] Read markers: continue working on ui --- .../api/session/room/model/RoomSummary.kt | 9 +- .../api/session/room/timeline/Timeline.kt | 18 +- .../database/mapper/RoomSummaryMapper.kt | 15 +- .../database/model/RoomSummaryEntity.kt | 3 +- .../query/ReadReceiptEntityQueries.kt | 7 + .../query/RoomSummaryEntityQueries.kt | 6 + .../matrix/android/internal/di/MatrixScope.kt | 2 +- .../android/internal/session/SessionModule.kt | 2 - .../android/internal/session/SessionScope.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 23 +- .../session/room/timeline/DefaultTimeline.kt | 137 ++++++----- .../room/timeline/DefaultTimelineService.kt | 3 +- .../room/timeline/TimelineHiddenReadMarker.kt | 96 ++++++++ .../session/sync/RoomFullyReadHandler.kt | 6 + .../core/ui/views/JumpToReadMarkerView.kt | 75 ++++++ .../riotx/core/ui/views/ReadMarkerView.kt | 86 +++++++ .../home/room/detail/DownloadFileState.kt | 25 ++ .../home/room/detail/RoomDetailActions.kt | 11 +- .../home/room/detail/RoomDetailFragment.kt | 112 +++++++-- .../home/room/detail/RoomDetailViewModel.kt | 178 +++++++------- .../home/room/detail/RoomDetailViewState.kt | 4 +- .../ScrollOnHighlightedEventCallback.kt | 2 +- .../timeline/TimelineEventController.kt | 69 ++++-- .../timeline/factory/EncryptedItemFactory.kt | 22 +- .../timeline/factory/EncryptionItemFactory.kt | 71 ------ .../timeline/factory/MessageItemFactory.kt | 223 +++++------------- .../timeline/factory/NoticeItemFactory.kt | 5 +- .../timeline/factory/TimelineItemFactory.kt | 11 +- .../timeline/format/NoticeEventFormatter.kt | 14 +- .../MessageInformationDataFactory.kt | 33 +-- .../helper/MessageItemAttributesFactory.kt | 58 +++++ .../helper/TimelineDisplayableEvents.kt | 27 +-- ...lineEventVisibilityStateChangedListener.kt | 9 +- .../detail/timeline/item/AbsMessageItem.kt | 108 ++++----- .../detail/timeline/item/BaseEventItem.kt | 3 + .../timeline/item/MessageImageVideoItem.kt | 12 +- .../detail/timeline/item/MessageTextItem.kt | 4 +- .../room/detail/timeline/item/NoticeItem.kt | 19 +- .../src/main/res/anim/unread_marker_anim.xml | 2 - .../main/res/layout/fragment_room_detail.xml | 82 ++++--- .../res/layout/item_timeline_event_base.xml | 17 +- .../item_timeline_event_base_noinfo.xml | 17 +- ...item_timeline_event_merged_header_stub.xml | 2 +- .../res/layout/view_jump_to_read_marker.xml | 41 ++++ .../src/main/res/layout/view_read_marker.xml | 58 +++++ 45 files changed, 1073 insertions(+), 656 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/{util => helper}/MessageInformationDataFactory.kt (82%) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt create mode 100644 vector/src/main/res/layout/view_jump_to_read_marker.xml create mode 100644 vector/src/main/res/layout/view_read_marker.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index aae72dd4..36aab8db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -35,9 +35,14 @@ data class RoomSummary( val highlightCount: Int = 0, val tags: List = emptyList(), val membership: Membership = Membership.NONE, - val versioningState: VersioningState = VersioningState.NONE + val versioningState: VersioningState = VersioningState.NONE, + val readMarkerId: String? = null ) { val isVersioned: Boolean get() = versioningState != VersioningState.NONE -} \ No newline at end of file + + val hasNewMessages: Boolean + get() = notificationCount != 0 +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 314c9f61..3f90d3cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -32,6 +32,8 @@ interface Timeline { var listener: Listener? + val isLive: Boolean + /** * This should be called before any other method after creating the timeline. It ensures the underlying database is open */ @@ -42,6 +44,10 @@ interface Timeline { */ fun dispose() + + fun restartWithEventId(eventId: String) + + /** * Check if the timeline can be enriched by paginating. * @param the direction to check in @@ -49,6 +55,7 @@ interface Timeline { */ fun hasMoreToLoad(direction: Direction): Boolean + /** * This is the main method to enrich the timeline with new data. * It will call the onUpdated method from [Listener] when the data will be processed. @@ -56,9 +63,16 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) - fun pendingEventCount() : Int + fun pendingEventCount(): Int + + fun failedToDeliverEventCount(): Int + + fun getIndexOfEvent(eventId: String?): Int? + + fun getTimelineEventAtIndex(index: Int): TimelineEvent? + + fun getTimelineEventWithId(eventId: String?): TimelineEvent? - fun failedToDeliverEventCount() : Int interface Listener { /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 03061c6e..cf829b44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -43,12 +43,12 @@ internal class RoomSummaryMapper @Inject constructor( //for now decrypt sync try { val result = cryptoService.decryptEvent(latestEvent.root, latestEvent.root.roomId + UUID.randomUUID().toString()) - latestEvent.root.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) + latestEvent.root.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) } catch (e: MXCryptoError) { } @@ -65,7 +65,8 @@ internal class RoomSummaryMapper @Inject constructor( notificationCount = roomSummaryEntity.notificationCount, tags = tags, membership = roomSummaryEntity.membership, - versioningState = roomSummaryEntity.versioningState + versioningState = roomSummaryEntity.versioningState, + readMarkerId = roomSummaryEntity.readMarkerId ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 6fe81f4c..dde01c37 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -35,7 +35,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var otherMemberIds: RealmList = RealmList(), var notificationCount: Int = 0, var highlightCount: Int = 0, - var tags: RealmList = RealmList() + var tags: RealmList = RealmList(), + var readMarkerId: String? = null ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt index acac4199..330d76fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import io.realm.Realm import io.realm.RealmQuery +import io.realm.RealmResults import io.realm.kotlin.where internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { @@ -28,6 +29,12 @@ internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, use .equalTo(ReadReceiptEntityFields.USER_ID, userId) } +internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery { + return realm.where() + .equalTo(ReadReceiptEntityFields.USER_ID, userId) +} + + internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity { return ReadReceiptEntity().apply { this.primaryKey = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index f2c26042..bfa3f2c5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -31,6 +31,12 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n return query } +internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { + return where(realm, roomId).findFirst() + ?: realm.createObject(RoomSummaryEntity::class.java, roomId) +} + + internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { return RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt index 9c9327df..032b645f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt @@ -21,4 +21,4 @@ import javax.inject.Scope @Scope @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) -annotation class MatrixScope \ No newline at end of file +internal annotation class MatrixScope \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index ab44a4aa..106a80ce 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -36,9 +36,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater -import im.vector.matrix.android.internal.session.room.DefaultRoomFactory import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater -import im.vector.matrix.android.internal.session.room.RoomFactory import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt index 37753fdf..964165e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionScope.kt @@ -21,4 +21,4 @@ import javax.inject.Scope @Scope @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) -annotation class SessionScope \ No newline at end of file +internal annotation class SessionScope \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index af05510c..9652faae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room.read import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntity @@ -25,13 +26,17 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction import io.realm.Realm +import io.realm.RealmConfiguration import timber.log.Timber import javax.inject.Inject @@ -50,7 +55,8 @@ private const val READ_RECEIPT = "m.read" internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI: RoomAPI, private val credentials: Credentials, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val roomFullyReadHandler: RoomFullyReadHandler ) : SetReadMarkersTask { override suspend fun execute(params: SetReadMarkersTask.Params) { @@ -74,12 +80,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) { Timber.w("Can't set read marker for local event ${params.fullyReadEventId}") } else { + updateReadMarker(params.roomId, fullyReadEventId) markers[READ_MARKER] = fullyReadEventId } } if (readReceiptEventId != null && !isEventRead(params.roomId, readReceiptEventId)) { - if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) { Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}") } else { @@ -95,6 +101,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } + private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst() @@ -106,12 +113,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI } } - private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { - monarchy.writeAsync { realm -> + private suspend fun updateReadMarker(roomId: String, eventId: String) { + monarchy.awaitTransaction { realm -> + roomFullyReadHandler.handle(realm, roomId, FullyReadContent(eventId)) + } + } + + private suspend fun updateNotificationCountIfNecessary(roomId: String, eventId: String) { + monarchy.awaitTransaction { realm -> val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == eventId if (isLatestReceived) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: return@writeAsync + ?: return@awaitTransaction roomSummary.notificationCount = 0 roomSummary.highlightCount = 0 } 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 26983a82..f14df5ad 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,36 +27,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain -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.EventAnnotationsSummaryEntity -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.ReadMarkerEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntity -import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields -import im.vector.matrix.android.internal.database.query.FilterContent -import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates -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.database.query.whereInRoom +import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.query.* import im.vector.matrix.android.internal.task.TaskConstraints 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 im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createUIHandler -import io.realm.ObjectChangeSet -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmObjectChangeListener -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -70,7 +49,7 @@ private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE internal class DefaultTimeline( private val roomId: String, - private val initialEventId: String? = null, + private var initialEventId: String? = null, private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, @@ -78,8 +57,9 @@ internal class DefaultTimeline( private val cryptoService: CryptoService, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts -) : Timeline, TimelineHiddenReadReceipts.Delegate { + private val hiddenReadReceipts: TimelineHiddenReadReceipts, + private val hiddenReadMarker: TimelineHiddenReadMarker +) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate { private companion object { val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") @@ -104,11 +84,9 @@ internal class DefaultTimeline( private lateinit var eventRelations: RealmResults private var roomEntity: RoomEntity? = null - private var readMarkerEntity: ReadMarkerEntity? = null private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN - private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsPaginationState = AtomicReference(PaginationState()) @@ -116,6 +94,9 @@ internal class DefaultTimeline( private val timelineID = UUID.randomUUID().toString() + override val isLive + get() = initialEventId == null + private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService) private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> @@ -124,10 +105,7 @@ internal class DefaultTimeline( } 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() - builtEventsIdMap.clear() + clearAllValues() } changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { @@ -176,29 +154,6 @@ internal class DefaultTimeline( if (hasChange) postSnapshot() } - private val readMarkerListener = RealmObjectChangeListener { readMarkerEntity: ReadMarkerEntity, _: ObjectChangeSet? -> - val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarkerEntity.eventId).findFirst() == null - var hasChange = false - if (isEventHidden) { - val hiddenEvent = readMarkerEntity.timelineEvent?.firstOrNull() ?: return@RealmObjectChangeListener - val displayIndex = hiddenEvent.root?.displayIndex - if (displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = liveEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should rebuild this one with marker - if (firstDisplayedEvent != null) { - hasChange = rebuildEvent(firstDisplayedEvent.eventId) { - it.copy(hasReadMarker = true) - } - } - } - } - if (hasChange) postSnapshot() - } - // Public methods ****************************************************************************** @@ -254,14 +209,10 @@ internal class DefaultTimeline( .findAllAsync() .also { it.addChangeListener(relationsListener) } - readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) - .findFirstAsync() - .also { it.addChangeListener(readMarkerListener) } - if (settings.buildReadReceipts) { hiddenReadReceipts.start(realm, liveEvents, this) } - + hiddenReadMarker.start(realm, liveEvents, this) isReady.set(true) } } @@ -276,10 +227,11 @@ internal class DefaultTimeline( roomEntity?.sendingTimelineEvents?.removeAllChangeListeners() eventRelations.removeAllChangeListeners() liveEvents.removeAllChangeListeners() - readMarkerEntity?.removeAllChangeListeners() + hiddenReadMarker.dispose() if (settings.buildReadReceipts) { hiddenReadReceipts.dispose() } + clearAllValues() backgroundRealm.getAndSet(null).also { it.close() } @@ -287,6 +239,27 @@ internal class DefaultTimeline( } } + override fun restartWithEventId(eventId: String) { + dispose() + initialEventId = eventId + start() + postSnapshot() + } + + override fun getIndexOfEvent(eventId: String?): Int? { + return builtEventsIdMap[eventId] + } + + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { + return builtEvents.getOrNull(index) + } + + override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { + return builtEventsIdMap[eventId]?.let { + getTimelineEventAtIndex(it) + } + } + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { return hasMoreInCache(direction) || !hasReachedEnd(direction) } @@ -303,6 +276,18 @@ internal class DefaultTimeline( postSnapshot() } + // TimelineHiddenReadMarker.Delegate + + override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean { + return rebuildEvent(eventId) { te -> + te.copy(hasReadMarker = hasReadMarker) + } + } + + override fun onReadMarkerUpdated() { + postSnapshot() + } + // Private methods ***************************************************************************** private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { @@ -423,8 +408,9 @@ internal class DefaultTimeline( prevDisplayIndex = initialDisplayIndex nextDisplayIndex = initialDisplayIndex - if (initialEventId != null && shouldFetchInitialEvent) { - fetchEvent(initialEventId) + val currentInitialEventId = initialEventId + if (currentInitialEventId != null && shouldFetchInitialEvent) { + fetchEvent(currentInitialEventId) } else { val count = Math.min(settings.initialSize, liveEvents.size) if (isLive) { @@ -571,10 +557,11 @@ internal class DefaultTimeline( } private fun findCurrentChunk(realm: Realm): ChunkEntity? { - return if (initialEventId == null) { + val currentInitialEventId = initialEventId + return if (currentInitialEventId == null) { ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) } else { - ChunkEntity.findIncludingEvent(realm, initialEventId) + ChunkEntity.findIncludingEvent(realm, currentInitialEventId) } } @@ -594,11 +581,23 @@ internal class DefaultTimeline( } private fun postSnapshot() { - val snapshot = createSnapshot() - val runnable = Runnable { listener?.onUpdated(snapshot) } - debouncer.debounce("post_snapshot", runnable, 50) + BACKGROUND_HANDLER.post { + val snapshot = createSnapshot() + val runnable = Runnable { listener?.onUpdated(snapshot) } + debouncer.debounce("post_snapshot", runnable, 50) + } } + private fun clearAllValues() { + prevDisplayIndex = DISPLAY_INDEX_UNKNOWN + nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + builtEvents.clear() + builtEventsIdMap.clear() + backwardsPaginationState.set(PaginationState()) + forwardsPaginationState.set(PaginationState()) + } + + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index b6cc80ca..59d37a80 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -59,7 +59,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv cryptoService, timelineEventMapper, settings, - TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings) + TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), + TimelineHiddenReadMarker(roomId) ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt new file mode 100644 index 00000000..532a6614 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadMarker.kt @@ -0,0 +1,96 @@ +/* + + * 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.matrix.android.internal.session.room.timeline + +import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm +import io.realm.RealmObjectChangeListener +import io.realm.RealmResults + +/** + * This class is responsible for handling the read marker for hidden events. + * When an hidden event has read marker, we want to transfer it on the first older displayed event. + * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. + */ +internal class TimelineHiddenReadMarker constructor(private val roomId: String) { + + interface Delegate { + fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean + fun onReadMarkerUpdated() + } + + private var previousDisplayedEventId: String? = null + private var readMarkerEntity: ReadMarkerEntity? = null + + private lateinit var liveEvents: RealmResults + private lateinit var delegate: Delegate + + private val readMarkerListener = RealmObjectChangeListener { readMarker, _ -> + var hasChange = false + previousDisplayedEventId?.also { + hasChange = delegate.rebuildEvent(it, false) + previousDisplayedEventId = null + } + val isEventHidden = liveEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, readMarker.eventId).findFirst() == null + if (isEventHidden) { + val hiddenEvent = readMarker.timelineEvent?.firstOrNull() + ?: return@RealmObjectChangeListener + val displayIndex = hiddenEvent.root?.displayIndex + if (displayIndex != null) { + // Then we are looking for the first displayable event after the hidden one + val firstDisplayedEvent = liveEvents.where() + .lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex) + .findFirst() + + // If we find one, we should rebuild this one with marker + if (firstDisplayedEvent != null) { + previousDisplayedEventId = firstDisplayedEvent.eventId + hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true) + } + } + } + if (hasChange) delegate.onReadMarkerUpdated() + } + + + /** + * Start the realm query subscription. Has to be called on an HandlerThread + */ + fun start(realm: Realm, liveEvents: RealmResults, delegate: Delegate) { + this.liveEvents = liveEvents + this.delegate = delegate + // We are looking for read receipts set on hidden events. + // We only accept those with a timelineEvent (so coming from pagination/sync). + readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId) + .findFirstAsync() + .also { it.addChangeListener(readMarkerListener) } + + } + + /** + * Dispose the realm query subscription. Has to be called on an HandlerThread + */ + fun dispose() { + this.readMarkerEntity?.removeAllChangeListeners() + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index f142ca06..9757d0f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.sync import im.vector.matrix.android.api.session.room.read.FullyReadContent import im.vector.matrix.android.internal.database.model.ReadMarkerEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where @@ -32,9 +33,14 @@ internal class RoomFullyReadHandler @Inject constructor() { return } Timber.v("Handle for roomId: $roomId eventId: ${content.eventId}") + + RoomSummaryEntity.getOrCreate(realm, roomId).apply { + readMarkerId = content.eventId + } val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { eventId = content.eventId } + // Remove the old marker if any readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null // Attach to timelineEvent if known diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt new file mode 100644 index 00000000..398d5252 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt @@ -0,0 +1,75 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ + +package im.vector.riotx.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import android.widget.RelativeLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.view_jump_to_read_marker.view.* +import me.gujun.android.span.span +import me.saket.bettermovementmethod.BetterLinkMovementMethod + +class JumpToReadMarkerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { + + interface Callback { + fun onJumpToReadMarkerClicked(readMarkerId: String) + fun onClearReadMarkerClicked() + } + + var callback: Callback? = null + + init { + setupView() + } + + private fun setupView() { + LinearLayout.inflate(context, R.layout.view_jump_to_read_marker, this) + setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color)) + jumpToReadMarkerLabelView.movementMethod = BetterLinkMovementMethod.getInstance() + isClickable = true + closeJumpToReadMarkerView.setOnClickListener { + visibility = View.GONE + callback?.onClearReadMarkerClicked() + } + } + + fun render(show: Boolean, readMarkerId: String?) { + isVisible = show + if (readMarkerId != null) { + jumpToReadMarkerLabelView.text = span(resources.getString(R.string.room_jump_to_first_unread)) { + textDecorationLine = "underline" + onClick = { callback?.onJumpToReadMarkerClicked(readMarkerId) } + } + } + + } + + +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt new file mode 100644 index 00000000..becab54d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -0,0 +1,86 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ + +package im.vector.riotx.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import im.vector.riotx.R +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import kotlinx.coroutines.* + +private const val DELAY_IN_MS = 1_500L + +class ReadMarkerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + interface Callback { + fun onReadMarkerDisplayed() + } + + private var callback: Callback? = null + private var callbackDispatcherJob: Job? = null + + fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) { + this.callback = readMarkerCallback + if (informationData.displayReadMarker) { + visibility = VISIBLE + callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { + delay(DELAY_IN_MS) + callback?.onReadMarkerDisplayed() + } + startAnimation() + } else { + visibility = INVISIBLE + } + + } + + fun unbind() { + this.callbackDispatcherJob?.cancel() + this.callback = null + this.animation?.cancel() + this.visibility = INVISIBLE + } + + private fun startAnimation() { + if (animation == null) { + animation = AnimationUtils.loadAnimation(context, R.anim.unread_marker_anim) + animation.startOffset = DELAY_IN_MS / 2 + animation.duration = DELAY_IN_MS / 2 + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation) { + } + + override fun onAnimationEnd(animation: Animation) { + visibility = INVISIBLE + } + + override fun onAnimationRepeat(animation: Animation) {} + }) + } + animation.start() + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt new file mode 100644 index 00000000..2426a41e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/DownloadFileState.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +import java.io.File + +data class DownloadFileState( + val mimeType: String, + val file: File?, + val throwable: Throwable? + ) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index e60bc422..70d0d59c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail import com.jaiselrahman.filepicker.model.MediaFile import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -27,15 +26,18 @@ sealed class RoomDetailActions { data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() - data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() + data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions() + data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions() data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions() - data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions() + data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions() + data class SetReadMarkerAction(val eventId: String) : RoomDetailActions() + object MarkAllAsRead : RoomDetailActions() data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions() - data class HandleTombstoneEvent(val event: Event): RoomDetailActions() + data class HandleTombstoneEvent(val event: Event) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() @@ -47,5 +49,4 @@ sealed class RoomDetailActions { object ClearSendQueue : RoomDetailActions() object ResendAll : RoomDetailActions() - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 19262fad..fd83a6f6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -28,7 +28,12 @@ import android.os.Parcelable import android.text.Editable import android.text.Spannable import android.text.TextUtils -import android.view.* +import android.view.HapticFeedbackConstants +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.Window import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.Toast @@ -46,7 +51,12 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -60,7 +70,13 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent @@ -77,9 +93,21 @@ import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp -import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.utils.* +import im.vector.riotx.core.ui.views.JumpToReadMarkerView +import im.vector.riotx.core.ui.views.NotificationAreaView +import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA +import im.vector.riotx.core.utils.allGranted +import im.vector.riotx.core.utils.checkPermissions +import im.vector.riotx.core.utils.copyToClipboard +import im.vector.riotx.core.utils.openCamera +import im.vector.riotx.core.utils.shareMedia +import im.vector.riotx.core.utils.toast import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter @@ -94,9 +122,18 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction +import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener -import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView @@ -134,7 +171,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callback, AutocompleteUserPresenter.Callback, - VectorInviteView.Callback { + VectorInviteView.Callback, + JumpToReadMarkerView.Callback { companion object { @@ -194,6 +232,7 @@ class RoomDetailFragment : override fun getMenuRes() = R.menu.menu_timeline private lateinit var actionViewModel: ActionsHandler + private lateinit var layoutManager: LinearLayoutManager @BindView(R.id.composerLayout) lateinit var composerLayout: TextComposerView @@ -211,6 +250,7 @@ class RoomDetailFragment : setupAttachmentButton() setupInviteView() setupNotificationView() + setupJumpToReadMarkerView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) } @@ -224,8 +264,12 @@ class RoomDetailFragment : } roomDetailViewModel.navigateToEvent.observeEvent(this) { - // - scrollOnHighlightedEventCallback.scheduleScrollTo(it) + val scrollPosition = timelineEventController.searchPositionOfEvent(it) + if (scrollPosition == null) { + scrollOnHighlightedEventCallback.scheduleScrollTo(it) + } else { + layoutManager.scrollToPosition(scrollPosition) + } } roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) { @@ -259,6 +303,10 @@ class RoomDetailFragment : } } + private fun setupJumpToReadMarkerView() { + jumpToReadMarkerView.callback = this + } + private fun setupNotificationView() { notificationAreaView.delegate = object : NotificationAreaView.Delegate { @@ -380,7 +428,7 @@ class RoomDetailFragment : private fun setupRecyclerView() { val epoxyVisibilityTracker = EpoxyVisibilityTracker() epoxyVisibilityTracker.attach(recyclerView) - val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) + layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) @@ -405,7 +453,7 @@ class RoomDetailFragment : R.drawable.ic_reply, object : RoomMessageTouchHelperCallback.QuickReplayHandler { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.informationData?.let { + (model as? AbsMessageItem)?.attributes?.informationData?.let { val eventId = it.eventId roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) } @@ -416,7 +464,7 @@ class RoomDetailFragment : is MessageFileItem, is MessageImageVideoItem, is MessageTextItem -> { - return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED } else -> false } @@ -585,7 +633,7 @@ class RoomDetailFragment : val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { - timelineEventController.setTimeline(state.timeline, state.eventId) + timelineEventController.setTimeline(state.timeline, state.highlightedEventId) inviteView.visibility = View.GONE val uid = session.myUserId val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) @@ -608,10 +656,12 @@ class RoomDetailFragment : composerLayout.visibility = View.GONE notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } + jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) } private fun renderRoomSummary(state: RoomDetailViewState) { state.asyncRoomSummary()?.let { + if (it.membership.isLeft()) { Timber.w("The room has been left") activity?.finish() @@ -684,7 +734,7 @@ class RoomDetailFragment : .show() } -// TimelineEventController.Callback ************************************************************ + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -696,7 +746,7 @@ class RoomDetailFragment : showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) } else { // Highlight and scroll to this event - roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, timelineEventController.searchPositionOfEvent(eventId))) + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(eventId, true)) } return true } @@ -716,7 +766,11 @@ class RoomDetailFragment : } override fun onEventVisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsVisible(event)) + } + + override fun onEventInvisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.TimelineEventTurnsInvisible(event)) } override fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) { @@ -836,7 +890,15 @@ class RoomDetailFragment : .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS") } -// AutocompleteUserPresenter.Callback + override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) { + val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() + val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem) + if (eventId != null) { + roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId)) + } + } + + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) @@ -1001,7 +1063,7 @@ class RoomDetailFragment : snack.show() } -// VectorInviteView.Callback + // VectorInviteView.Callback override fun onAcceptInvite() { notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) @@ -1012,4 +1074,16 @@ class RoomDetailFragment : notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId) roomDetailViewModel.process(RoomDetailActions.RejectInvite) } + + // JumpToReadMarkerView.Callback + + override fun onJumpToReadMarkerClicked(readMarkerId: String) { + roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) + } + + override fun onClearReadMarkerClicked() { + roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead) + } + + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 607f999e..bbdb7ab6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl @@ -58,6 +59,8 @@ import im.vector.riotx.features.command.CommandParser import im.vector.riotx.features.command.ParsedCommand import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents import im.vector.riotx.features.settings.VectorPreferences +import io.reactivex.Observable +import io.reactivex.functions.Function3 import io.reactivex.rxkotlin.subscribeBy import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer @@ -75,7 +78,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId - private val displayedEventsObservable = BehaviorRelay.create() + private val invisibleEventsObservable = BehaviorRelay.create() + private val visibleEventsObservable = BehaviorRelay.create() private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { TimelineSettings(30, false, true, TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, userPreferencesProvider.shouldShowReadReceipts()) } else { @@ -109,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() + observeJumpToReadMarkerViewVisibility() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } @@ -116,30 +121,37 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.SendMedia -> handleSendMedia(action) - is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailActions.SendReaction -> handleSendReaction(action) - is RoomDetailActions.AcceptInvite -> handleAcceptInvite() - is RoomDetailActions.RejectInvite -> handleRejectInvite() - is RoomDetailActions.RedactAction -> handleRedactEvent(action) - is RoomDetailActions.UndoReaction -> handleUndoReact(action) - is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailActions.EnterEditMode -> handleEditAction(action) - is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) - is RoomDetailActions.DownloadFile -> handleDownloadFile(action) - is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailActions.ResendMessage -> handleResendEvent(action) - is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) - is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() - is RoomDetailActions.ResendAll -> handleResendAll() - else -> Timber.e("Unhandled Action: $action") + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.SendMedia -> handleSendMedia(action) + is RoomDetailActions.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailActions.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailActions.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailActions.SendReaction -> handleSendReaction(action) + is RoomDetailActions.AcceptInvite -> handleAcceptInvite() + is RoomDetailActions.RejectInvite -> handleRejectInvite() + is RoomDetailActions.RedactAction -> handleRedactEvent(action) + is RoomDetailActions.UndoReaction -> handleUndoReact(action) + is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailActions.EnterEditMode -> handleEditAction(action) + is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailActions.EnterReplyMode -> handleReplyAction(action) + is RoomDetailActions.DownloadFile -> handleDownloadFile(action) + is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailActions.ResendMessage -> handleResendEvent(action) + is RoomDetailActions.RemoveFailedEcho -> handleRemove(action) + is RoomDetailActions.ClearSendQueue -> handleClearSendQueue() + is RoomDetailActions.ResendAll -> handleResendAll() + is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action) + is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead() + else -> Timber.e("Unhandled Action: $action") } } + private fun handleEventInvisible(action: RoomDetailActions.TimelineEventTurnsInvisible) { + invisibleEventsObservable.accept(action) + } + private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() ?: return @@ -444,14 +456,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.sendMedias(attachments) } - private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { + private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) { if (action.event.root.sendState.isSent()) { //ignore pending/local events - displayedEventsObservable.accept(action) + visibleEventsObservable.accept(action) } //We need to update this with the related m.replace also (to move read receipt) action.event.annotations?.editSummary?.sourceEvents?.forEach { room.getTimeLineEvent(it)?.let { event -> - displayedEventsObservable.accept(RoomDetailActions.EventDisplayed(event)) + visibleEventsObservable.accept(RoomDetailActions.TimelineEventTurnsVisible(event)) } } } @@ -494,11 +506,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } - data class DownloadFileState( - val mimeType: String, - val file: File?, - val throwable: Throwable? - ) private fun handleDownloadFile(action: RoomDetailActions.DownloadFile) { session.downloadFile( @@ -530,53 +537,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleNavigateToEvent(action: RoomDetailActions.NavigateToEvent) { val targetEventId = action.eventId - - if (action.position != null) { - // Event is already in RAM - withState { - if (it.eventId == targetEventId) { - // ensure another click on the same permalink will also do a scroll - setState { - copy( - eventId = null - ) - } - } - - setState { - copy( - eventId = targetEventId - ) - } - } - - _navigateToEvent.postLiveEvent(targetEventId) - } else { - // change timeline - timeline.dispose() - timeline = room.createTimeline(targetEventId, timelineSettings) - timeline.start() - - withState { - if (it.eventId == targetEventId) { - // ensure another click on the same permalink will also do a scroll - setState { - copy( - eventId = null - ) - } - } - - setState { - copy( - eventId = targetEventId, - timeline = this@RoomDetailViewModel.timeline - ) - } - } - - _navigateToEvent.postLiveEvent(targetEventId) + val indexOfEvent = timeline.getIndexOfEvent(targetEventId) + if (indexOfEvent == null) { + // Event is not already in RAM + timeline.restartWithEventId(targetEventId) } + if (action.highlight) { + setState { copy(highlightedEventId = targetEventId) } + } + _navigateToEvent.postLiveEvent(targetEventId) } private fun handleResendEvent(action: RoomDetailActions.ResendMessage) { @@ -622,22 +591,36 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. - displayedEventsObservable + visibleEventsObservable .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val readMarkerVisible = actions.find { it.event.hasReadMarker } != null val mostRecentEvent = actions.maxBy { it.event.displayIndex } mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) - if (readMarkerVisible) { - room.setReadMarker(eventId, callback = object : MatrixCallback {}) - } } }) .disposeOnClear() } + private fun handleSetReadMarkerAction(action: RoomDetailActions.SetReadMarkerAction) = withState { state -> + var readMarkerId = action.eventId + if (readMarkerId == state.asyncRoomSummary()?.readMarkerId) { + val indexOfEvent = timeline.getIndexOfEvent(action.eventId) + // force to set the read marker on the next event + if (indexOfEvent != null) { + timeline.getTimelineEventAtIndex(indexOfEvent - 1)?.root?.eventId?.also { eventIdOfNext -> + readMarkerId = eventIdOfNext + } + } + } + room.setReadMarker(readMarkerId, callback = object : MatrixCallback {}) + } + + private fun handleMarkAllAsRead() { + room.markAllAsRead(object : MatrixCallback {}) + } + private fun observeSyncState() { session.rx() .liveSyncState() @@ -649,6 +632,39 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } + private fun observeJumpToReadMarkerViewVisibility() { + Observable + .combineLatest( + room.rx().liveRoomSummary(), + visibleEventsObservable.distinctUntilChanged(), + isEventVisibleObservable { it.hasReadMarker }.startWith(false), + Function3 { roomSummary, currentVisibleEvent, isReadMarkerViewVisible -> + val readMarkerId = roomSummary.readMarkerId + if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) { + false + } else { + val readMarkerPosition = timeline.getIndexOfEvent(readMarkerId) + ?: Int.MAX_VALUE + val currentVisibleEventPosition = timeline.getIndexOfEvent(currentVisibleEvent.event.root.eventId) + ?: Int.MIN_VALUE + readMarkerPosition > currentVisibleEventPosition + } + } + ) + .distinctUntilChanged() + .subscribe { + setState { copy(showJumpToReadMarker = it) } + } + .disposeOnClear() + } + + private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable { + return Observable.merge( + visibleEventsObservable.filter { filterEvent(it.event) }.map { true }, + invisibleEventsObservable.filter { filterEvent(it.event) }.map { false } + ) + } + private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index d8358efe..5e36cf42 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -51,7 +51,9 @@ data class RoomDetailViewState( val isEncrypted: Boolean = false, val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, - val syncState: SyncState = SyncState.IDLE + val syncState: SyncState = SyncState.IDLE, + val showJumpToReadMarker: Boolean = false, + val highlightedEventId: String? = null ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 43828b0e..cf483090 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -38,7 +38,7 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { // Note: Offset will be from the bottom, since the layoutManager is reversed - layoutManager.scrollToPositionWithOffset(positionToScroll, 120) + layoutManager.scrollToPosition(position) } scheduledEventId.set(null) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 3c212d61..ffc573a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -49,11 +49,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val avatarRenderer: AvatarRenderer, @TimelineEventControllerHandler - private val backgroundHandler: Handler, - userPreferencesProvider: UserPreferencesProvider + private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { + fun onEventInvisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) @@ -81,6 +81,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) + fun onReadMarkerLongDisplayed(informationData: MessageInformationData) } interface UrlClickCallback { @@ -140,8 +141,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } - private val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() - init { requestModelBuild() } @@ -247,7 +246,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] - val nextEvent = items.nextDisplayableEvent(currentPosition, showHiddenEvents) + val nextEvent = items.nextOrNull(currentPosition) val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() @@ -327,24 +326,50 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return shouldAdd } - fun searchPositionOfEvent(eventId: String): Int? { - synchronized(modelCache) { - // Search in the cache - modelCache.forEachIndexed { idx, cacheItemData -> - if (cacheItemData?.eventId == eventId) { - return idx - } + fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) { + // Search in the cache + var realPosition = 0 + for (i in 0 until modelCache.size) { + val itemCache = modelCache[i] + if (itemCache?.eventId == eventId) { + return realPosition + } + if (itemCache?.eventModel != null) { + realPosition++ + } + if (itemCache?.mergedHeaderModel != null) { + realPosition++ + } + if (itemCache?.formattedDayModel != null) { + realPosition++ } - - return null } + return null } -} -private data class CacheItemData( - val localId: Long, - val eventId: String?, - val eventModel: EpoxyModel<*>? = null, - val mergedHeaderModel: MergedHeaderItem? = null, - val formattedDayModel: DaySeparatorItem? = null -) + fun searchEventIdAtPosition(position: Int): String? = synchronized(modelCache) { + var offsetValue = 0 + for (i in 0 until position) { + val itemCache = modelCache[i] + if (itemCache?.eventModel == null) { + offsetValue-- + } + if (itemCache?.mergedHeaderModel != null) { + offsetValue++ + } + if (itemCache?.formattedDayModel != null) { + offsetValue++ + } + } + return modelCache.getOrNull(position - offsetValue)?.eventId + } + + private data class CacheItemData( + val localId: Long, + val eventId: String?, + val eventModel: EpoxyModel<*>? = null, + val mergedHeaderModel: MergedHeaderItem? = null, + val formattedDayModel: DaySeparatorItem? = null + ) + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 080565cd..938ac467 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -16,7 +16,6 @@ package im.vector.riotx.features.home.room.detail.timeline.factory -import android.view.View import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -24,11 +23,11 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import me.gujun.android.span.span import javax.inject.Inject @@ -36,7 +35,7 @@ import javax.inject.Inject class EncryptedItemFactory @Inject constructor(private val messageInformationDataFactory: MessageInformationDataFactory, private val colorProvider: ColorProvider, private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { + private val attributesFactory: MessageItemAttributesFactory) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?, @@ -65,22 +64,13 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat // TODO This is not correct format for error, change it val informationData = messageInformationDataFactory.create(event, nextEvent) + val attributes = attributesFactory.create(null, informationData, callback) return MessageTextItem_() + .attributes(attributes) .message(spannableStr) - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) .urlClickCallback(callback) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEncryptedMessageClicked(informationData, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, null, view) - ?: false - } + } else -> null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt deleted file mode 100644 index 4a3f50c4..00000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ /dev/null @@ -1,71 +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.riotx.features.home.room.detail.timeline.factory - -import im.vector.matrix.android.api.session.events.model.Event -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.timeline.TimelineEvent -import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent -import im.vector.riotx.R -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import javax.inject.Inject - -class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer) { - - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.BaseCallback?): NoticeItem? { - - val text = buildNoticeText(event.root, event.senderName) ?: return null - val informationData = MessageInformationData( - eventId = event.root.eventId ?: "?", - senderId = event.root.senderId ?: "", - sendState = event.root.sendState, - avatarUrl = event.senderAvatar(), - memberName = event.senderName(), - showInformation = false - ) - return NoticeItem_() - .avatarRenderer(avatarRenderer) - .noticeText(text) - .informationData(informationData) - .highlighted(highlight) - .baseCallback(callback) - } - - private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { - return when { - EventType.ENCRYPTION == event.getClearType() -> { - val content = event.content.toModel() ?: return null - stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm) - } - else -> null - } - - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f3a93a8d..57baf4fe 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -47,27 +47,14 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.linkify.VectorLinkify import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.resources.UserPreferencesProvider import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.item.BlankItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem -import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem -import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem -import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer @@ -75,14 +62,13 @@ import me.gujun.android.span.span import javax.inject.Inject class MessageItemFactory @Inject constructor( - private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val htmlRenderer: Lazy, private val stringProvider: StringProvider, - private val emojiCompatFontProvider: EmojiCompatFontProvider, private val imageContentRenderer: ImageContentRenderer, private val messageInformationDataFactory: MessageInformationDataFactory, + private val messageItemAttributesFactory: MessageItemAttributesFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val noticeItemFactory: NoticeItemFactory) { @@ -98,36 +84,41 @@ class MessageItemFactory @Inject constructor( if (event.root.isRedacted()) { //message is redacted - return buildRedactedItem(informationData, highlight, callback) + val attributes = messageItemAttributesFactory.create(null, informationData, callback) + return buildRedactedItem(attributes, highlight) } val messageContent: MessageContent = event.getLastMessageContent() - ?: //Malformed content, we should echo something on screen - return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) + ?: //Malformed content, we should echo something on screen + return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) if (messageContent.relatesTo?.type == RelationType.REPLACE - || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should it when debugging as a notice event return noticeItemFactory.create(event, highlight, callback) } + val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) + // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, - informationData, - highlight, - callback) + informationData, + highlight, + callback, + attributes) is MessageTextContent -> buildTextMessageItem(messageContent, - informationData, - highlight, - callback) - is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback) + informationData, + highlight, + callback, + attributes) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, highlight) } } @@ -135,55 +126,29 @@ class MessageItemFactory @Inject constructor( private fun buildAudioMessageItem(messageContent: MessageAudioContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageFileItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) - .readReceiptsCallback(callback) .filename(messageContent.body) .iconRes(R.drawable.filetype_audio) - .reactionPillCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view: View -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) .clickListener( DebouncedClickListener(View.OnClickListener { callback?.onAudioMessageClicked(messageContent) })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildFileMessageItem(messageContent: MessageFileContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageFileItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageFileItem? { return MessageFileItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) .filename(messageContent.body) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) .iconRes(R.drawable.filetype_attachment) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } .clickListener( DebouncedClickListener(View.OnClickListener { _ -> callback?.onFileMessageClicked(informationData.eventId, messageContent) @@ -200,7 +165,8 @@ class MessageItemFactory @Inject constructor( private fun buildImageMessageItem(messageContent: MessageImageContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageImageVideoItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( @@ -215,42 +181,29 @@ class MessageItemFactory @Inject constructor( rotation = messageContent.info?.rotation ) return MessageImageVideoItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) + .attributes(attributes) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .playable(messageContent.info?.mimeType == "image/gif") - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) .mediaData(data) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) .clickListener( DebouncedClickListener(View.OnClickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) })) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildVideoMessageItem(messageContent: MessageVideoContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageImageVideoItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, @@ -267,33 +220,20 @@ class MessageItemFactory @Inject constructor( ) return MessageImageVideoItem_() + .attributes(attributes) .imageContentRenderer(imageContentRenderer) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) .playable(true) - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) .mediaData(thumbnailData) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildTextMessageItem(messageContent: MessageTextContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageTextItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { val bodyToUse = messageContent.formattedBody?.let { htmlRenderer.get().render(it.trim()) @@ -310,24 +250,10 @@ class MessageItemFactory @Inject constructor( message(linkifiedBody) } } - .avatarRenderer(avatarRenderer) - .informationData(informationData) - .colorProvider(colorProvider) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) .urlClickCallback(callback) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - //click on the text - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } + //click on the text } private fun annotateWithEdited(linkifiedBody: CharSequence, @@ -356,16 +282,17 @@ class MessageItemFactory @Inject constructor( //nop } }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) return spannable } private fun buildNoticeMessageItem(messageContent: MessageNoticeContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageTextItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { val message = messageContent.body.let { val formattedBody = span { @@ -376,34 +303,17 @@ class MessageItemFactory @Inject constructor( linkifyBody(formattedBody, callback) } return MessageTextItem_() - .avatarRenderer(avatarRenderer) + .attributes(attributes) .message(message) - .colorProvider(colorProvider) - .informationData(informationData) .highlighted(highlight) - .avatarCallback(callback) - .reactionPillCallback(callback) .urlClickCallback(callback) - .readReceiptsCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .memberClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onMemberNameClicked(informationData) - })) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } private fun buildEmoteMessageItem(messageContent: MessageEmoteContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): MessageTextItem? { + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { val message = messageContent.body.let { val formattedBody = "* ${informationData.memberName} $it" @@ -418,43 +328,16 @@ class MessageItemFactory @Inject constructor( message(message) } } - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) - .reactionPillCallback(callback) - .readReceiptsCallback(callback) .urlClickCallback(callback) - .emojiTypeFace(emojiCompatFontProvider.typeface) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, messageContent, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view) - ?: false - } } - private fun buildRedactedItem(informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?): RedactedMessageItem? { + private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, + highlight: Boolean): RedactedMessageItem? { return RedactedMessageItem_() - .avatarRenderer(avatarRenderer) - .colorProvider(colorProvider) - .informationData(informationData) + .attributes(attributes) .highlighted(highlight) - .avatarCallback(callback) - .readReceiptsCallback(callback) - .cellClickListener( - DebouncedClickListener(View.OnClickListener { view -> - callback?.onEventCellClicked(informationData, null, view) - })) - .longClickListener { view -> - return@longClickListener callback?.onEventLongClicked(informationData, null, view) - ?: false - } } private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index f73a2001..6955cf35 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -20,12 +20,9 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.helper.senderName -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index b1ae595e..9913f219 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -20,17 +20,11 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.core.epoxy.EmptyItem_ import im.vector.riotx.core.epoxy.VectorEpoxyModel -import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar -import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_ -import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory import timber.log.Timber import javax.inject.Inject class TimelineItemFactory @Inject constructor(private val messageItemFactory: MessageItemFactory, - private val encryptionItemFactory: EncryptionItemFactory, private val encryptedItemFactory: EncryptedItemFactory, private val noticeItemFactory: NoticeItemFactory, private val defaultItemFactory: DefaultItemFactory, @@ -40,6 +34,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me nextEvent: TimelineEvent?, eventIdToHighlight: String?, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { + val highlight = event.root.eventId == eventIdToHighlight val computedModel = try { @@ -55,11 +50,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_HANGUP, EventType.CALL_ANSWER, EventType.REACTION, - EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) + EventType.REDACTION, + EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) // Crypto - EventType.ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 05ce7a9c..2fcc1744 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.api.session.room.model.call.CallInviteContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent import im.vector.riotx.R import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.room.detail.timeline.helper.senderName @@ -41,6 +42,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.ENCRYPTION -> formatEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.MESSAGE, EventType.REACTION, EventType.REDACTION -> formatDebug(timelineEvent.root) @@ -60,6 +62,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> formatCallEvent(event, senderName) + EventType.ENCRYPTION -> formatEncryptionEvent(event, senderName) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName) else -> { Timber.v("Type $type not handled by this formatter") @@ -96,7 +99,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { val historyVisibility = event.getClearContent().toModel()?.historyVisibility - ?: return null + ?: return null val formattedVisibility = when (historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -146,7 +149,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.senderId, prevEventContent?.displayName, eventContent?.displayName) + event.senderId, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -173,7 +176,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin 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.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) event.stateKey.isNullOrEmpty() -> @@ -209,4 +212,9 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin } } + private fun formatEncryptionEvent(event: Event, senderName: String?): CharSequence? { + val eventContent: EncryptionEventContent = event.getClearContent().toModel() ?: return null + return stringProvider.getString(R.string.notice_end_to_end, senderName, eventContent.algorithm) + } + } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt similarity index 82% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 71a7549b..1e978bcf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -1,20 +1,22 @@ /* - * 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. + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -package im.vector.riotx.features.home.room.detail.timeline.util +package im.vector.riotx.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType @@ -62,7 +64,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: "")) } - val displayReadMarker = event.hasReadMarker && event.readReceipts.find { it.user.userId == session.myUserId } == null + val displayReadMarker = event.hasReadMarker + && event.readReceipts.find { it.user.userId == session.myUserId } == null return MessageInformationData( eventId = eventId, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt new file mode 100644 index 00000000..47b5094c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageItemAttributesFactory.kt @@ -0,0 +1,58 @@ +/* + + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + + */ +package im.vector.riotx.features.home.room.detail.timeline.helper + +import android.view.View +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.riotx.EmojiCompatFontProvider +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import javax.inject.Inject + +class MessageItemAttributesFactory @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val colorProvider: ColorProvider, + private val emojiCompatFontProvider: EmojiCompatFontProvider) { + + fun create(messageContent: MessageContent?, informationData: MessageInformationData, callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { + return AbsMessageItem.Attributes( + informationData = informationData, + avatarRenderer = avatarRenderer, + colorProvider = colorProvider, + itemLongClickListener = View.OnLongClickListener { view -> + callback?.onEventLongClicked(informationData, messageContent, view) ?: false + }, + itemClickListener = DebouncedClickListener(View.OnClickListener { view -> + callback?.onEventCellClicked(informationData, messageContent, view) + }), + memberClickListener = DebouncedClickListener(View.OnClickListener { view -> + callback?.onMemberNameClicked(informationData) + }), + reactionPillCallback = callback, + avatarCallback = callback, + readReceiptsCallback = callback, + emojiTypeFace = emojiCompatFontProvider.typeface + ) + + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index fa0a71bd..b9c9d992 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -49,29 +49,6 @@ object TimelineDisplayableEvents { ) } -fun TimelineEvent.isDisplayable(showHiddenEvent: Boolean): Boolean { - val allowed = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES.takeIf { showHiddenEvent } - ?: TimelineDisplayableEvents.DISPLAYABLE_TYPES - if (!allowed.contains(root.type)) { - return false - } - if (root.content.isNullOrEmpty()) { - return false - } - //Edits should be filtered out! - if (EventType.MESSAGE == root.type - && root.content.toModel()?.relatesTo?.type == RelationType.REPLACE) { - return false - } - return true -} -// -//fun List.filterDisplayableEvents(): List { -// return this.filter { -// it.isDisplayable() -// } -//} - fun TimelineEvent.senderAvatar(): String? { // We might have no avatar when user leave, so we try to get it from prevContent return senderAvatar @@ -131,10 +108,10 @@ fun List.prevSameTypeEvents(index: Int, minSize: Int): List.nextDisplayableEvent(index: Int, showHiddenEvent: Boolean): TimelineEvent? { +fun List.nextOrNull(index: Int): TimelineEvent? { return if (index >= size - 1) { null } else { - subList(index + 1, this.size).firstOrNull { it.isDisplayable(showHiddenEvent) } + subList(index + 1, this.size).firstOrNull() } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index 95d9b6f4..eb3dc44e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -28,9 +28,10 @@ class TimelineEventVisibilityStateChangedListener(private val callback: Timeline override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { callback?.onEventVisible(event) + } else if (visibilityState == VisibilityState.INVISIBLE) { + callback?.onEventInvisible(event) } } - } @@ -40,9 +41,9 @@ class MergedTimelineEventVisibilityStateChangedListener(private val callback: Ti override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { - events.forEach { - callback?.onEventVisible(it) - } + events.forEach { callback?.onEventVisible(it) } + } else if (visibilityState == VisibilityState.INVISIBLE) { + events.forEach { callback?.onEventInvisible(it) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 570daf66..5431b4ca 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -32,6 +32,7 @@ import com.airbnb.epoxy.EpoxyAttribute import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DimensionUtils.dpToPx @@ -43,63 +44,42 @@ import im.vector.riotx.features.ui.getMessageTextColor abstract class AbsMessageItem : BaseEventItem() { @EpoxyAttribute - lateinit var informationData: MessageInformationData - - @EpoxyAttribute - lateinit var avatarRenderer: AvatarRenderer - - @EpoxyAttribute - lateinit var colorProvider: ColorProvider - - @EpoxyAttribute - var longClickListener: View.OnLongClickListener? = null - - @EpoxyAttribute - var cellClickListener: View.OnClickListener? = null - - @EpoxyAttribute - var memberClickListener: View.OnClickListener? = null - - @EpoxyAttribute - var emojiTypeFace: Typeface? = null - - @EpoxyAttribute - var reactionPillCallback: TimelineEventController.ReactionPillCallback? = null - - @EpoxyAttribute - var avatarCallback: TimelineEventController.AvatarCallback? = null - - @EpoxyAttribute - var readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + lateinit var attributes: Attributes private val _avatarClickListener = DebouncedClickListener(View.OnClickListener { - avatarCallback?.onAvatarClicked(informationData) + attributes.avatarCallback?.onAvatarClicked(attributes.informationData) }) private val _memberNameClickListener = DebouncedClickListener(View.OnClickListener { - avatarCallback?.onMemberNameClicked(informationData) + attributes.avatarCallback?.onMemberNameClicked(attributes.informationData) }) private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener { - readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) + attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts) }) + private val _readMarkerCallback = object : ReadMarkerView.Callback { + override fun onReadMarkerDisplayed() { + attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData) + } + } + var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { - reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true) + attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true) } override fun onUnReacted(reactionButton: ReactionButton) { - reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false) + attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false) } override fun onLongClick(reactionButton: ReactionButton) { - reactionPillCallback?.onLongClickOnReactionPill(informationData, reactionButton.reactionString) + attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString) } } override fun bind(holder: H) { super.bind(holder) - if (informationData.showInformation) { + if (attributes.informationData.showInformation) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { val size = dpToPx(avatarStyle.avatarSizeDP, holder.view.context) height = size @@ -110,13 +90,13 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.visibility = View.VISIBLE holder.memberNameView.setOnClickListener(_memberNameClickListener) holder.timeView.visibility = View.VISIBLE - holder.timeView.text = informationData.time - holder.memberNameView.text = informationData.memberName - avatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView) - holder.view.setOnClickListener(cellClickListener) - holder.view.setOnLongClickListener(longClickListener) - holder.avatarImageView.setOnLongClickListener(longClickListener) - holder.memberNameView.setOnLongClickListener(longClickListener) + holder.timeView.text = attributes.informationData.time + holder.memberNameView.text = attributes.informationData.memberName + attributes.avatarRenderer.render(attributes.informationData.avatarUrl, attributes.informationData.senderId, attributes.informationData.memberName?.toString(), holder.avatarImageView) + holder.view.setOnClickListener(attributes.itemClickListener) + holder.view.setOnLongClickListener(attributes.itemLongClickListener) + holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) + holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) } else { holder.avatarImageView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null) @@ -128,11 +108,10 @@ abstract class AbsMessageItem : BaseEventItem() { holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } + holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) + holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback) - holder.readMarkerView.isVisible = informationData.displayReadMarker - holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) - - if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { + if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false } else { //inflate if needed @@ -144,7 +123,7 @@ abstract class AbsMessageItem : BaseEventItem() { //clear all reaction buttons (but not the Flow helper!) holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true } val idToRefInFlow = ArrayList() - informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction -> + attributes.informationData.orderedReactionList?.chunked(8)?.firstOrNull()?.forEachIndexed { index, reaction -> (holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton -> reactionButton.isVisible = true reactionButton.reactedListener = reactionClickListener @@ -152,7 +131,7 @@ abstract class AbsMessageItem : BaseEventItem() { idToRefInFlow.add(reactionButton.id) reactionButton.reactionString = reaction.key reactionButton.reactionCount = reaction.count - reactionButton.emojiTypeFace = emojiTypeFace + reactionButton.emojiTypeFace = attributes.emojiTypeFace reactionButton.setChecked(reaction.addedByMe) reactionButton.isEnabled = reaction.synced } @@ -163,27 +142,48 @@ abstract class AbsMessageItem : BaseEventItem() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) { holder.reactionFlowHelper?.requestLayout() } - holder.reactionWrapper?.setOnLongClickListener(longClickListener) + holder.reactionWrapper?.setOnLongClickListener(attributes.itemLongClickListener) } } + override fun unbind(holder: H) { + holder.readMarkerView.unbind() + super.unbind(holder) + } + open fun shouldShowReactionAtBottom(): Boolean { return true } protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) { - root.isClickable = informationData.sendState.isSent() - val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState - textView?.setTextColor(colorProvider.getMessageTextColor(state)) - failureIndicator?.isVisible = informationData.sendState.hasFailed() + root.isClickable = attributes.informationData.sendState.isSent() + val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState + textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state)) + failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed() } + /** + * This class holds all the common attributes for message items. + */ + data class Attributes( + val informationData: MessageInformationData, + val avatarRenderer: AvatarRenderer, + val colorProvider: ColorProvider, + val itemLongClickListener: View.OnLongClickListener? = null, + val itemClickListener: View.OnClickListener? = null, + val memberClickListener: View.OnClickListener? = null, + val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, + val avatarCallback: TimelineEventController.AvatarCallback? = null, + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + val emojiTypeFace: Typeface? = null + ) + abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) + val readMarkerView by bind(R.id.readMarkerView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 843f52b3..5621f604 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -24,7 +24,10 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.platform.CheckableView +import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.DimensionUtils.dpToPx +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController /** * Children must override getViewType() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 6f713b17..94e48358 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -43,21 +43,21 @@ abstract class MessageImageVideoItem : AbsMessageItem() { holder.messageView.setTextFuture(textFuture) renderSendState(holder.messageView, holder.messageView) - holder.messageView.setOnClickListener(cellClickListener) - holder.messageView.setOnLongClickListener(longClickListener) + holder.messageView.setOnClickListener(attributes.itemClickListener) + holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) findPillsAndProcess { it.bind(holder.messageView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index dd42dc7b..aad090db 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -19,10 +19,10 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView -import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R +import im.vector.riotx.core.ui.views.ReadMarkerView import im.vector.riotx.core.ui.views.ReadReceiptsView import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.features.home.AvatarRenderer @@ -54,6 +54,12 @@ abstract class NoticeItem : BaseEventItem() { readReceiptsCallback?.onReadReceiptsClicked(informationData.readReceipts) }) + private val _readMarkerCallback = object : ReadMarkerView.Callback { + override fun onReadMarkerDisplayed() { + readReceiptsCallback?.onReadMarkerLongDisplayed(informationData) + } + } + override fun bind(holder: Holder) { super.bind(holder) holder.noticeTextView.text = noticeText @@ -61,12 +67,17 @@ abstract class NoticeItem : BaseEventItem() { informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString() - ?: informationData.senderId, + ?: informationData.senderId, holder.avatarImageView ) holder.view.setOnLongClickListener(longClickListener) holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer, _readReceiptsClickListener) - holder.readMarkerView.isVisible = informationData.displayReadMarker + holder.readMarkerView.bindView(informationData, _readMarkerCallback) + } + + override fun unbind(holder: Holder) { + holder.readMarkerView.unbind() + super.unbind(holder) } override fun getViewType() = STUB_ID @@ -75,7 +86,7 @@ abstract class NoticeItem : BaseEventItem() { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) val readReceiptsView by bind(R.id.readReceiptsView) - val readMarkerView by bind(R.id.readMarkerView) + val readMarkerView by bind(R.id.readMarkerView) } companion object { diff --git a/vector/src/main/res/anim/unread_marker_anim.xml b/vector/src/main/res/anim/unread_marker_anim.xml index 0c7ddab3..9e61c80c 100644 --- a/vector/src/main/res/anim/unread_marker_anim.xml +++ b/vector/src/main/res/anim/unread_marker_anim.xml @@ -1,7 +1,6 @@ \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 1d376282..d7ce9963 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -6,6 +6,29 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + - - - - - + - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" /> + + - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index ea4cfd5d..4eb9be0b 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -128,17 +128,20 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/readMarkerView" app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index ad6999c5..fc4a527d 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -58,18 +58,21 @@ android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/readMarkerView" app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintStart_toStartOf="parent" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml index 1dd5a611..46c84aa4 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_header_stub.xml @@ -40,7 +40,7 @@ android:layout_width="0dp" android:layout_height="1dp" android:layout_marginTop="4dp" - android:background="?attr/colorAccent" + android:background="?attr/riotx_header_panel_background" app:layout_constraintEnd_toEndOf="@id/itemMergedExpandTextView" app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView" app:layout_constraintTop_toBottomOf="@id/itemMergedExpandTextView" /> diff --git a/vector/src/main/res/layout/view_jump_to_read_marker.xml b/vector/src/main/res/layout/view_jump_to_read_marker.xml new file mode 100644 index 00000000..35e14a64 --- /dev/null +++ b/vector/src/main/res/layout/view_jump_to_read_marker.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/vector/src/main/res/layout/view_read_marker.xml b/vector/src/main/res/layout/view_read_marker.xml new file mode 100644 index 00000000..e3cbc6ba --- /dev/null +++ b/vector/src/main/res/layout/view_read_marker.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + +