Timeline: continue fixing issues + read marker
This commit is contained in:
parent
3066d5f303
commit
88fb9667a3
@ -21,13 +21,14 @@ import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
|||||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
|
||||||
class RxRoom(private val room: Room) {
|
class RxRoom(private val room: Room) {
|
||||||
|
|
||||||
fun liveRoomSummary(): Observable<RoomSummary> {
|
fun liveRoomSummary(): Observable<RoomSummary> {
|
||||||
return room.liveRoomSummary().asObservable()
|
return room.getRoomSummaryLive().asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveRoomMemberIds(): Observable<List<String>> {
|
fun liveRoomMemberIds(): Observable<List<String>> {
|
||||||
@ -39,7 +40,15 @@ class RxRoom(private val room: Room) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
|
fun liveTimelineEvent(eventId: String): Observable<TimelineEvent> {
|
||||||
return room.liveTimeLineEvent(eventId).asObservable()
|
return room.getTimeLineEventLive(eventId).asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun liveReadMarker(): Observable<Optional<String>> {
|
||||||
|
return room.getReadMarkerLive().asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun liveReadReceipt(): Observable<Optional<String>> {
|
||||||
|
return room.getMyReadReceiptLive().asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
|
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
|
||||||
|
@ -47,7 +47,7 @@ interface Room :
|
|||||||
* A live [RoomSummary] associated with the room
|
* A live [RoomSummary] associated with the room
|
||||||
* You can observe this summary to get dynamic data from this room.
|
* You can observe this summary to get dynamic data from this room.
|
||||||
*/
|
*/
|
||||||
fun liveRoomSummary(): LiveData<RoomSummary>
|
fun getRoomSummaryLive(): LiveData<RoomSummary>
|
||||||
|
|
||||||
fun roomSummary(): RoomSummary?
|
fun roomSummary(): RoomSummary?
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.read
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
|
import im.vector.matrix.android.api.util.Optional
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
||||||
@ -40,12 +41,24 @@ interface ReadService {
|
|||||||
*/
|
*/
|
||||||
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
|
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an event is already read, ie. your read receipt is set on a more recent event.
|
||||||
|
*/
|
||||||
fun isEventRead(eventId: String): Boolean
|
fun isEventRead(eventId: String): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a nullable read marker for the room.
|
* Returns a live read marker id for the room.
|
||||||
*/
|
*/
|
||||||
fun getReadMarkerLive(): LiveData<String?>
|
fun getReadMarkerLive(): LiveData<Optional<String>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a live read receipt id for the room.
|
||||||
|
*/
|
||||||
|
fun getMyReadReceiptLive(): LiveData<Optional<String>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a live list of read receipts for a given event
|
||||||
|
* @param eventId: the event
|
||||||
|
*/
|
||||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||||
}
|
}
|
@ -36,5 +36,5 @@ interface TimelineService {
|
|||||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||||
|
|
||||||
|
|
||||||
fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent>
|
fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent>
|
||||||
}
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package im.vector.matrix.android.api.util
|
||||||
|
|
||||||
|
data class Optional<T : Any> constructor(private val value: T?) {
|
||||||
|
|
||||||
|
fun get(): T {
|
||||||
|
return value!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrNull(): T? {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOrElse(fn: () -> T): T {
|
||||||
|
return value ?: fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T : Any> from(value: T?): Optional<T> {
|
||||||
|
return Optional(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -53,7 +53,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||||||
RelationService by relationService,
|
RelationService by relationService,
|
||||||
MembershipService by roomMembersService {
|
MembershipService by roomMembersService {
|
||||||
|
|
||||||
override fun liveRoomSummary(): LiveData<RoomSummary> {
|
override fun getRoomSummaryLive(): LiveData<RoomSummary> {
|
||||||
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
|
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
|
||||||
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
|
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
|||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||||
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.internal.database.RealmLiveData
|
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||||
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
@ -83,24 +84,33 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
|
|||||||
var isEventRead = false
|
var isEventRead = false
|
||||||
monarchy.doWithRealm {
|
monarchy.doWithRealm {
|
||||||
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
||||||
?: return@doWithRealm
|
?: return@doWithRealm
|
||||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
||||||
?: return@doWithRealm
|
?: return@doWithRealm
|
||||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||||
?: Int.MIN_VALUE
|
?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
||||||
?: Int.MAX_VALUE
|
?: Int.MAX_VALUE
|
||||||
isEventRead = eventToCheckIndex <= readReceiptIndex
|
isEventRead = eventToCheckIndex <= readReceiptIndex
|
||||||
}
|
}
|
||||||
return isEventRead
|
return isEventRead
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReadMarkerLive(): LiveData<String?> {
|
override fun getReadMarkerLive(): LiveData<Optional<String>> {
|
||||||
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||||
ReadMarkerEntity.where(realm, roomId)
|
ReadMarkerEntity.where(realm, roomId)
|
||||||
}
|
}
|
||||||
return Transformations.map(liveRealmData) { results ->
|
return Transformations.map(liveRealmData) { results ->
|
||||||
results.firstOrNull()?.eventId
|
Optional.from(results.firstOrNull()?.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
|
||||||
|
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||||
|
ReadReceiptEntity.where(realm, roomId = roomId, userId = credentials.userId)
|
||||||
|
}
|
||||||
|
return Transformations.map(liveRealmData) { results ->
|
||||||
|
Optional.from(results.firstOrNull()?.eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.read
|
package im.vector.matrix.android.internal.session.room.read
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||||
@ -76,29 +75,49 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
|||||||
|
|
||||||
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
|
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
|
||||||
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
|
if (LocalEchoEventFactory.isLocalEchoId(fullyReadEventId)) {
|
||||||
Timber.w("Can't set read marker for local event ${params.fullyReadEventId}")
|
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
||||||
} else {
|
} else {
|
||||||
updateReadMarker(params.roomId, fullyReadEventId)
|
|
||||||
markers[READ_MARKER] = fullyReadEventId
|
markers[READ_MARKER] = fullyReadEventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readReceiptEventId != null
|
if (readReceiptEventId != null
|
||||||
&& !isEventRead(params.roomId, readReceiptEventId)) {
|
&& !isEventRead(params.roomId, readReceiptEventId)) {
|
||||||
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
|
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
|
||||||
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
|
Timber.w("Can't set read receipt for local event $readReceiptEventId")
|
||||||
} else {
|
} else {
|
||||||
updateNotificationCountIfNecessary(params.roomId, readReceiptEventId)
|
|
||||||
markers[READ_RECEIPT] = readReceiptEventId
|
markers[READ_RECEIPT] = readReceiptEventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (markers.isEmpty()) {
|
if (markers.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
updateDatabase(params.roomId, markers)
|
||||||
executeRequest<Unit> {
|
executeRequest<Unit> {
|
||||||
apiCall = roomAPI.sendReadMarker(params.roomId, markers)
|
apiCall = roomAPI.sendReadMarker(params.roomId, markers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun updateDatabase(roomId: String, markers: HashMap<String, String>) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
val readMarkerId = markers[READ_MARKER]
|
||||||
|
val readReceiptId = markers[READ_RECEIPT]
|
||||||
|
|
||||||
|
if (readMarkerId != null) {
|
||||||
|
roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId))
|
||||||
|
}
|
||||||
|
if (readReceiptId != null) {
|
||||||
|
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId
|
||||||
|
if (isLatestReceived) {
|
||||||
|
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||||
|
?: return@awaitTransaction
|
||||||
|
roomSummary.notificationCount = 0
|
||||||
|
roomSummary.highlightCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean {
|
private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
@ -111,36 +130,36 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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@awaitTransaction
|
|
||||||
roomSummary.notificationCount = 0
|
|
||||||
roomSummary.highlightCount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isEventRead(roomId: String, eventId: String): Boolean {
|
private fun isEventRead(roomId: String, eventId: String): Boolean {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
|
val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
|
||||||
?: return false
|
?: return false
|
||||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||||
?: return false
|
?: return false
|
||||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||||
?: Int.MIN_VALUE
|
?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
||||||
?: Int.MAX_VALUE
|
?: Int.MAX_VALUE
|
||||||
eventToCheckIndex <= readReceiptIndex
|
eventToCheckIndex <= readReceiptIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SetReadMarkersTask.Params.fullyReadEventId(): String? {
|
||||||
|
if (fullyReadEventId != null) {
|
||||||
|
return this.fullyReadEventId
|
||||||
|
} else {
|
||||||
|
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
|
||||||
|
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst()
|
||||||
|
return if (readMarker?.eventId == readReceipt?.eventId) {
|
||||||
|
readReceiptEventId
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -73,7 +73,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent> {
|
override fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent> {
|
||||||
val liveData = RealmLiveData(monarchy.realmConfiguration) {
|
val liveData = RealmLiveData(monarchy.realmConfiguration) {
|
||||||
TimelineEventEntity.where(it, eventId = eventId)
|
TimelineEventEntity.where(it, eventId = eventId)
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
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.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
@ -23,7 +23,7 @@ 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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
||||||
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||||
import im.vector.matrix.android.internal.database.helper.add
|
import im.vector.matrix.android.internal.database.helper.add
|
||||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||||
|
@ -184,7 +184,7 @@ class PushrulesConditionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {
|
class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {
|
||||||
override fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent> {
|
override fun getTimeLineEventLive(eventId: String): LiveData<TimelineEvent> {
|
||||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +193,7 @@ class PushrulesConditionTest {
|
|||||||
return _numberOfJoinedMembers
|
return _numberOfJoinedMembers
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun liveRoomSummary(): LiveData<RoomSummary> {
|
override fun getRoomSummaryLive(): LiveData<RoomSummary> {
|
||||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
private const val DELAY_IN_MS = 1_500L
|
private const val DELAY_IN_MS = 1_500L
|
||||||
@ -36,30 +36,34 @@ class ReadMarkerView @JvmOverloads constructor(
|
|||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onReadMarkerDisplayed()
|
fun onReadMarkerLongBound()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var eventId: String? = null
|
||||||
private var callback: Callback? = null
|
private var callback: Callback? = null
|
||||||
private var callbackDispatcherJob: Job? = null
|
private var callbackDispatcherJob: Job? = null
|
||||||
|
|
||||||
fun bindView(displayReadMarker: Boolean, readMarkerCallback: Callback) {
|
fun bindView(eventId: String?, hasReadMarker: Boolean, displayReadMarker: Boolean, readMarkerCallback: Callback) {
|
||||||
|
this.eventId = eventId
|
||||||
this.callback = readMarkerCallback
|
this.callback = readMarkerCallback
|
||||||
if (displayReadMarker) {
|
if (displayReadMarker) {
|
||||||
visibility = VISIBLE
|
|
||||||
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
delay(DELAY_IN_MS)
|
|
||||||
callback?.onReadMarkerDisplayed()
|
|
||||||
}
|
|
||||||
startAnimation()
|
startAnimation()
|
||||||
} else {
|
} else {
|
||||||
visibility = INVISIBLE
|
this.animation?.cancel()
|
||||||
|
this.visibility = INVISIBLE
|
||||||
|
}
|
||||||
|
if (hasReadMarker) {
|
||||||
|
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
delay(DELAY_IN_MS)
|
||||||
|
callback?.onReadMarkerLongBound()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() {
|
||||||
this.callbackDispatcherJob?.cancel()
|
this.callbackDispatcherJob?.cancel()
|
||||||
this.callback = null
|
this.callback = null
|
||||||
|
this.eventId = null
|
||||||
this.animation?.cancel()
|
this.animation?.cancel()
|
||||||
this.visibility = INVISIBLE
|
this.visibility = INVISIBLE
|
||||||
}
|
}
|
||||||
@ -80,6 +84,7 @@ class ReadMarkerView @JvmOverloads constructor(
|
|||||||
override fun onAnimationRepeat(animation: Animation) {}
|
override fun onAnimationRepeat(animation: Animation) {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
visibility = VISIBLE
|
||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,10 @@ internal class Debouncer(private val handler: Handler) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelAll() {
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
|
||||||
private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
|
private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
|
||||||
val chained = Runnable {
|
val chained = Runnable {
|
||||||
handler.post(r)
|
handler.post(r)
|
||||||
@ -41,4 +45,6 @@ internal class Debouncer(private val handler: Handler) {
|
|||||||
runnables[identifier] = chained
|
runnables[identifier] = chained
|
||||||
handler.postDelayed(chained, millis)
|
handler.postDelayed(chained, millis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -44,7 +44,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.util.Pair
|
import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
@ -312,17 +311,21 @@ class RoomDetailFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
debouncer.cancelAll()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupJumpToBottomView() {
|
private fun setupJumpToBottomView() {
|
||||||
jumpToBottomView.isVisible = false
|
jumpToBottomView.visibility = View.INVISIBLE
|
||||||
jumpToBottomView.setOnClickListener {
|
jumpToBottomView.setOnClickListener {
|
||||||
|
jumpToBottomView.visibility = View.INVISIBLE
|
||||||
withState(roomDetailViewModel) { state ->
|
withState(roomDetailViewModel) { state ->
|
||||||
recyclerView.stopScroll()
|
|
||||||
if (state.timeline?.isLive == false) {
|
if (state.timeline?.isLive == false) {
|
||||||
state.timeline.restartWithEventId(null)
|
state.timeline.restartWithEventId(null)
|
||||||
} else {
|
} else {
|
||||||
layoutManager.scrollToPosition(0)
|
layoutManager.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
jumpToBottomView.isVisible = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -398,17 +401,17 @@ class RoomDetailFragment :
|
|||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse(messageContent.formattedBody
|
val document = parser.parse(messageContent.formattedBody
|
||||||
?: messageContent.body)
|
?: messageContent.body)
|
||||||
formattedBody = eventHtmlRenderer.render(document)
|
formattedBody = eventHtmlRenderer.render(document)
|
||||||
}
|
}
|
||||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||||
?: nonFormattedBody
|
?: nonFormattedBody
|
||||||
|
|
||||||
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
||||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
|
|
||||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||||
|
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
@ -437,9 +440,9 @@ class RoomDetailFragment :
|
|||||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||||
REACTION_SELECT_REQUEST_CODE -> {
|
REACTION_SELECT_REQUEST_CODE -> {
|
||||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||||
?: return
|
?: return
|
||||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||||
?: return
|
?: return
|
||||||
//TODO check if already reacted with that?
|
//TODO check if already reacted with that?
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||||
}
|
}
|
||||||
@ -490,33 +493,33 @@ class RoomDetailFragment :
|
|||||||
|
|
||||||
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
||||||
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
||||||
R.drawable.ic_reply,
|
R.drawable.ic_reply,
|
||||||
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
||||||
val eventId = it.eventId
|
val eventId = it.eventId
|
||||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
return when (model) {
|
return when (model) {
|
||||||
is MessageFileItem,
|
is MessageFileItem,
|
||||||
is MessageImageVideoItem,
|
is MessageImageVideoItem,
|
||||||
is MessageTextItem -> {
|
is MessageTextItem -> {
|
||||||
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
|
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||||
touchHelper.attachToRecyclerView(recyclerView)
|
touchHelper.attachToRecyclerView(recyclerView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateJumpToBottomViewVisibility() {
|
private fun updateJumpToBottomViewVisibility() {
|
||||||
debouncer.debounce("jump_to_bottom_visibility", 100, Runnable {
|
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
|
||||||
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
|
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
|
||||||
if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) {
|
if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) {
|
||||||
jumpToBottomView.show()
|
jumpToBottomView.show()
|
||||||
@ -684,7 +687,7 @@ class RoomDetailFragment :
|
|||||||
val summary = state.asyncRoomSummary()
|
val summary = state.asyncRoomSummary()
|
||||||
val inviter = state.asyncInviter()
|
val inviter = state.asyncInviter()
|
||||||
if (summary?.membership == Membership.JOIN) {
|
if (summary?.membership == Membership.JOIN) {
|
||||||
timelineEventController.setTimeline(state.timeline, state.highlightedEventId)
|
timelineEventController.update(state.timeline, state.highlightedEventId, state.hideReadMarker)
|
||||||
inviteView.visibility = View.GONE
|
inviteView.visibility = View.GONE
|
||||||
val uid = session.myUserId
|
val uid = session.myUserId
|
||||||
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
||||||
@ -747,7 +750,6 @@ class RoomDetailFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
|
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
|
||||||
when (sendMessageResult) {
|
when (sendMessageResult) {
|
||||||
is SendMessageResult.MessageSent -> {
|
is SendMessageResult.MessageSent -> {
|
||||||
@ -945,15 +947,15 @@ class RoomDetailFragment :
|
|||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadMarkerLongDisplayed(informationData: MessageInformationData) {
|
override fun onReadMarkerLongDisplayed() = withState(roomDetailViewModel) { state ->
|
||||||
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
|
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
|
||||||
val eventId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
|
val nextReadMarkerId = timelineEventController.searchEventIdAtPosition(firstVisibleItem)
|
||||||
if (eventId != null) {
|
if (nextReadMarkerId != null) {
|
||||||
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(eventId))
|
roomDetailViewModel.process(RoomDetailActions.SetReadMarkerAction(nextReadMarkerId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutocompleteUserPresenter.Callback
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||||
|
@ -40,13 +40,13 @@ 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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
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.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.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
@ -62,6 +62,7 @@ import im.vector.riotx.features.command.ParsedCommand
|
|||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.functions.BiFunction
|
||||||
import io.reactivex.functions.Function3
|
import io.reactivex.functions.Function3
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
@ -116,6 +117,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
observeEventDisplayedActions()
|
observeEventDisplayedActions()
|
||||||
observeSummaryState()
|
observeSummaryState()
|
||||||
observeJumpToReadMarkerViewVisibility()
|
observeJumpToReadMarkerViewVisibility()
|
||||||
|
observeReadMarkerVisibility()
|
||||||
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
||||||
timeline.start()
|
timeline.start()
|
||||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||||
@ -156,7 +158,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
|
|
||||||
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
|
private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
|
||||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
val roomId = tombstoneContent.replacementRoom ?: ""
|
val roomId = tombstoneContent.replacementRoom ?: ""
|
||||||
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
||||||
@ -303,7 +305,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
|
|
||||||
//is original event a reply?
|
//is original event a reply?
|
||||||
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
//TODO check if same content?
|
//TODO check if same content?
|
||||||
room.getTimeLineEvent(inReplyTo)?.let {
|
room.getTimeLineEvent(inReplyTo)?.let {
|
||||||
@ -312,12 +314,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
} else {
|
} else {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val existingBody = messageContent?.body ?: ""
|
val existingBody = messageContent?.body ?: ""
|
||||||
if (existingBody != action.text) {
|
if (existingBody != action.text) {
|
||||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
||||||
?: "", messageContent?.type
|
?: "", messageContent?.type
|
||||||
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Same message content, do not send edition")
|
Timber.w("Same message content, do not send edition")
|
||||||
}
|
}
|
||||||
@ -332,7 +334,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
is SendMode.QUOTE -> {
|
is SendMode.QUOTE -> {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val textMsg = messageContent?.body
|
val textMsg = messageContent?.body
|
||||||
|
|
||||||
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
val finalText = legacyRiotQuoteText(textMsg, action.text)
|
||||||
@ -635,29 +637,30 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeJumpToReadMarkerViewVisibility() {
|
private fun observeJumpToReadMarkerViewVisibility() {
|
||||||
Observable
|
Observable.combineLatest(
|
||||||
.combineLatest(
|
room.rx().liveRoomSummary()
|
||||||
room.rx().liveRoomSummary().map {
|
.map {
|
||||||
val readMarkerId = it.readMarkerId
|
val readMarkerId = it.readMarkerId
|
||||||
if (readMarkerId == null) {
|
if (readMarkerId == null) {
|
||||||
Option.empty()
|
Option.empty()
|
||||||
} else {
|
} else {
|
||||||
val timelineEvent = room.getTimeLineEvent(readMarkerId)
|
val readMarkerIndex = room.getTimeLineEvent(readMarkerId)?.displayIndex
|
||||||
Option.fromNullable(timelineEvent)
|
?: Int.MIN_VALUE
|
||||||
}
|
Option.just(readMarkerIndex)
|
||||||
}.distinctUntilChanged(),
|
|
||||||
visibleEventsObservable.distinctUntilChanged(),
|
|
||||||
isEventVisibleObservable { it.hasReadMarker }.startWith(false),
|
|
||||||
Function3<Option<TimelineEvent>, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerEvent, currentVisibleEvent, isReadMarkerViewVisible ->
|
|
||||||
if (readMarkerEvent.isEmpty() || isReadMarkerViewVisible) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
val readMarkerPosition = readMarkerEvent.map { it.displayIndex }.getOrElse { Int.MIN_VALUE }
|
|
||||||
val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex
|
|
||||||
readMarkerPosition < currentVisibleEventPosition
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
.distinctUntilChanged(),
|
||||||
|
visibleEventsObservable.distinctUntilChanged(),
|
||||||
|
isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it },
|
||||||
|
Function3<Option<Int>, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerIndex, currentVisibleEvent, isReadMarkerViewVisible ->
|
||||||
|
if (readMarkerIndex.isEmpty() || isReadMarkerViewVisible) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex
|
||||||
|
readMarkerIndex.getOrElse { Int.MIN_VALUE } < currentVisibleEventPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.subscribe {
|
.subscribe {
|
||||||
setState { copy(showJumpToReadMarker = it) }
|
setState { copy(showJumpToReadMarker = it) }
|
||||||
@ -682,6 +685,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeReadMarkerVisibility() {
|
||||||
|
Observable
|
||||||
|
.combineLatest(
|
||||||
|
room.rx().liveReadMarker(),
|
||||||
|
room.rx().liveReadReceipt(),
|
||||||
|
BiFunction<Optional<String>, Optional<String>, Boolean> { readMarker, readReceipt ->
|
||||||
|
readMarker.getOrNull() == readReceipt.getOrNull()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.throttleLast(250, TimeUnit.MILLISECONDS)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.startWith(false)
|
||||||
|
.subscribe {
|
||||||
|
setState { copy(hideReadMarker = it) }
|
||||||
|
}
|
||||||
|
.disposeOnClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun observeSummaryState() {
|
private fun observeSummaryState() {
|
||||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||||
if (summary.membership == Membership.INVITE) {
|
if (summary.membership == Membership.INVITE) {
|
||||||
|
@ -53,7 +53,8 @@ data class RoomDetailViewState(
|
|||||||
val tombstoneEventHandling: Async<String> = Uninitialized,
|
val tombstoneEventHandling: Async<String> = Uninitialized,
|
||||||
val syncState: SyncState = SyncState.IDLE,
|
val syncState: SyncState = SyncState.IDLE,
|
||||||
val showJumpToReadMarker: Boolean = false,
|
val showJumpToReadMarker: Boolean = false,
|
||||||
val highlightedEventId: String? = null
|
val highlightedEventId: String? = null,
|
||||||
|
val hideReadMarker: Boolean = false
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||||
|
@ -26,7 +26,7 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
|||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
Timber.v("On inserted $count count at position: $position")
|
Timber.v("On inserted $count count at position: $position")
|
||||||
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) {
|
if (position == 0 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) {
|
||||||
layoutManager.scrollToPosition(0)
|
layoutManager.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,6 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
|||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.core.epoxy.LoadingItem_
|
import im.vector.riotx.core.epoxy.LoadingItem_
|
||||||
import im.vector.riotx.core.extensions.localDateTime
|
import im.vector.riotx.core.extensions.localDateTime
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
||||||
@ -80,7 +78,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
|
|
||||||
interface ReadReceiptsCallback {
|
interface ReadReceiptsCallback {
|
||||||
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
|
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
|
||||||
fun onReadMarkerLongDisplayed(informationData: MessageInformationData)
|
fun onReadMarkerLongDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UrlClickCallback {
|
interface UrlClickCallback {
|
||||||
@ -142,7 +140,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTimeline(timeline: Timeline?, eventIdToHighlight: String?) {
|
fun update(timeline: Timeline?, eventIdToHighlight: String?, hideReadMarker: Boolean) {
|
||||||
if (this.timeline != timeline) {
|
if (this.timeline != timeline) {
|
||||||
this.timeline = timeline
|
this.timeline = timeline
|
||||||
this.timeline?.listener = this
|
this.timeline?.listener = this
|
||||||
@ -155,22 +153,30 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestModelBuild = false
|
||||||
if (this.eventIdToHighlight != eventIdToHighlight) {
|
if (this.eventIdToHighlight != eventIdToHighlight) {
|
||||||
// Clear cache to force a refresh
|
// Clear cache to force a refresh
|
||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
for (i in 0 until modelCache.size) {
|
for (i in 0 until modelCache.size) {
|
||||||
if (modelCache[i]?.eventId == eventIdToHighlight
|
if (modelCache[i]?.eventId == eventIdToHighlight
|
||||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||||
modelCache[i] = null
|
modelCache[i] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.eventIdToHighlight = eventIdToHighlight
|
this.eventIdToHighlight = eventIdToHighlight
|
||||||
|
requestModelBuild = true
|
||||||
|
}
|
||||||
|
if (this.hideReadMarker != hideReadMarker) {
|
||||||
|
this.hideReadMarker = hideReadMarker
|
||||||
|
requestModelBuild = true
|
||||||
|
}
|
||||||
|
if (requestModelBuild) {
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var hideReadMarker: Boolean = false
|
||||||
private var eventIdToHighlight: String? = null
|
private var eventIdToHighlight: String? = null
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
@ -224,8 +230,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||||
// We then are sure we always have items up to date.
|
// We then are sure we always have items up to date.
|
||||||
if (modelCache[position] == null
|
if (modelCache[position] == null
|
||||||
|| modelCache[position]?.mergedHeaderModel != null
|
|| modelCache[position]?.mergedHeaderModel != null
|
||||||
|| modelCache[position]?.formattedDayModel != null) {
|
|| modelCache[position]?.formattedDayModel != null) {
|
||||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -251,7 +257,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||||
|
|
||||||
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also {
|
val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, hideReadMarker, callback).also {
|
||||||
it.id(event.localId)
|
it.id(event.localId)
|
||||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
|
hideReadMarker: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
exception: Exception? = null): DefaultItem? {
|
exception: Exception? = null): DefaultItem? {
|
||||||
val text = if (exception == null) {
|
val text = if (exception == null) {
|
||||||
@ -39,7 +40,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||||||
"an exception occurred when rendering the event ${event.root.eventId}"
|
"an exception occurred when rendering the event ${event.root.eventId}"
|
||||||
}
|
}
|
||||||
|
|
||||||
val informationData = informationDataFactory.create(event, null)
|
val informationData = informationDataFactory.create(event, null, hideReadMarker)
|
||||||
|
|
||||||
return DefaultItem_()
|
return DefaultItem_()
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
@ -41,6 +41,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
|
hideReadMarker: Boolean,
|
||||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||||
event.root.eventId ?: return null
|
event.root.eventId ?: return null
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
|
|
||||||
// TODO This is not correct format for error, change it
|
// TODO This is not correct format for error, change it
|
||||||
|
|
||||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker)
|
||||||
val attributes = attributesFactory.create(null, informationData, callback)
|
val attributes = attributesFactory.create(null, informationData, callback)
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
@ -51,18 +51,22 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
|||||||
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
var highlighted = false
|
|
||||||
var showReadMarker = false
|
|
||||||
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
||||||
if (prevSameTypeEvents.isEmpty()) {
|
if (prevSameTypeEvents.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
var highlighted = false
|
||||||
|
var readMarkerId: String? = null
|
||||||
|
var showReadMarker = false
|
||||||
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
||||||
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
||||||
mergedEvents.forEach { mergedEvent ->
|
mergedEvents.forEach { mergedEvent ->
|
||||||
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||||
highlighted = true
|
highlighted = true
|
||||||
}
|
}
|
||||||
|
if (readMarkerId == null && mergedEvent.hasReadMarker) {
|
||||||
|
readMarkerId = mergedEvent.root.eventId
|
||||||
|
}
|
||||||
if (!showReadMarker && mergedEvent.displayReadMarker(sessionHolder.getActiveSession().myUserId)) {
|
if (!showReadMarker && mergedEvent.displayReadMarker(sessionHolder.getActiveSession().myUserId)) {
|
||||||
showReadMarker = true
|
showReadMarker = true
|
||||||
}
|
}
|
||||||
@ -81,7 +85,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
|||||||
// => handle case where paginating from mergeable events and we get more
|
// => handle case where paginating from mergeable events and we get more
|
||||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||||
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
||||||
?: true
|
?: true
|
||||||
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
collapsedEventIds.addAll(mergedEventIds)
|
collapsedEventIds.addAll(mergedEventIds)
|
||||||
@ -97,12 +101,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
|||||||
mergeItemCollapseStates[event.localId] = it
|
mergeItemCollapseStates[event.localId] = it
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
},
|
},
|
||||||
showReadMarker = showReadMarker
|
readMarkerId = readMarkerId,
|
||||||
|
showReadMarker = isCollapsed && showReadMarker,
|
||||||
|
readReceiptsCallback = callback
|
||||||
)
|
)
|
||||||
MergedHeaderItem_()
|
MergedHeaderItem_()
|
||||||
.id(mergeId)
|
.id(mergeId)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.highlighted(highlighted)
|
.highlighted(isCollapsed && highlighted)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.also {
|
.also {
|
||||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||||
|
@ -76,11 +76,12 @@ class MessageItemFactory @Inject constructor(
|
|||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
|
hideReadMarker: Boolean,
|
||||||
callback: TimelineEventController.Callback?
|
callback: TimelineEventController.Callback?
|
||||||
): VectorEpoxyModel<*>? {
|
): VectorEpoxyModel<*>? {
|
||||||
event.root.eventId ?: return null
|
event.root.eventId ?: return null
|
||||||
|
|
||||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
val informationData = messageInformationDataFactory.create(event, nextEvent, hideReadMarker)
|
||||||
|
|
||||||
if (event.root.isRedacted()) {
|
if (event.root.isRedacted()) {
|
||||||
//message is redacted
|
//message is redacted
|
||||||
@ -97,7 +98,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||||
) {
|
) {
|
||||||
// This is an edit event, we should it when debugging as a notice event
|
// This is an edit event, we should it when debugging as a notice event
|
||||||
return noticeItemFactory.create(event, highlight, callback)
|
return noticeItemFactory.create(event, highlight, hideReadMarker, callback)
|
||||||
}
|
}
|
||||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||||
|
|
||||||
|
@ -36,9 +36,11 @@ class NoticeItemFactory @Inject constructor(
|
|||||||
|
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
|
hideReadMarker: Boolean,
|
||||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||||
|
|
||||||
val formattedText = eventFormatter.format(event) ?: return null
|
val formattedText = eventFormatter.format(event) ?: return null
|
||||||
val informationData = informationDataFactory.create(event, null)
|
val informationData = informationDataFactory.create(event, null, hideReadMarker)
|
||||||
val attributes = NoticeItem.Attributes(
|
val attributes = NoticeItem.Attributes(
|
||||||
avatarRenderer = avatarRenderer,
|
avatarRenderer = avatarRenderer,
|
||||||
informationData = informationData,
|
informationData = informationData,
|
||||||
|
@ -33,13 +33,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
|
hideReadMarker: Boolean,
|
||||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||||
|
|
||||||
val highlight = event.root.eventId == eventIdToHighlight
|
val highlight = event.root.eventId == eventIdToHighlight
|
||||||
|
|
||||||
val computedModel = try {
|
val computedModel = try {
|
||||||
when (event.root.getClearType()) {
|
when (event.root.getClearType()) {
|
||||||
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback)
|
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback)
|
||||||
// State and call
|
// State and call
|
||||||
EventType.STATE_ROOM_TOMBSTONE,
|
EventType.STATE_ROOM_TOMBSTONE,
|
||||||
EventType.STATE_ROOM_NAME,
|
EventType.STATE_ROOM_NAME,
|
||||||
@ -51,22 +52,22 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
EventType.CALL_ANSWER,
|
EventType.CALL_ANSWER,
|
||||||
EventType.REACTION,
|
EventType.REACTION,
|
||||||
EventType.REDACTION,
|
EventType.REDACTION,
|
||||||
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
|
EventType.ENCRYPTION -> noticeItemFactory.create(event, highlight, hideReadMarker, callback)
|
||||||
// State room create
|
// State room create
|
||||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||||
// Crypto
|
// Crypto
|
||||||
EventType.ENCRYPTED -> {
|
EventType.ENCRYPTED -> {
|
||||||
if (event.root.isRedacted()) {
|
if (event.root.isRedacted()) {
|
||||||
// Redacted event, let the MessageItemFactory handle it
|
// Redacted event, let the MessageItemFactory handle it
|
||||||
messageItemFactory.create(event, nextEvent, highlight, callback)
|
messageItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback)
|
||||||
} else {
|
} else {
|
||||||
encryptedItemFactory.create(event, nextEvent, highlight, callback)
|
encryptedItemFactory.create(event, nextEvent, highlight, hideReadMarker, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unhandled event types (yet)
|
// Unhandled event types (yet)
|
||||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||||
EventType.STICKER -> defaultItemFactory.create(event, highlight, callback)
|
EventType.STICKER -> defaultItemFactory.create(event, highlight, hideReadMarker, callback)
|
||||||
else -> {
|
else -> {
|
||||||
Timber.v("Type ${event.root.getClearType()} not handled")
|
Timber.v("Type ${event.root.getClearType()} not handled")
|
||||||
null
|
null
|
||||||
@ -74,7 +75,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "failed to create message item")
|
Timber.e(e, "failed to create message item")
|
||||||
defaultItemFactory.create(event, highlight, callback, e)
|
defaultItemFactory.create(event, highlight, hideReadMarker, callback, e)
|
||||||
}
|
}
|
||||||
return (computedModel ?: EmptyItem_())
|
return (computedModel ?: EmptyItem_())
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val colorProvider: ColorProvider) {
|
private val colorProvider: ColorProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
fun create(event: TimelineEvent, nextEvent: TimelineEvent?, hideReadMarker: Boolean): MessageInformationData {
|
||||||
// Non nullability has been tested before
|
// Non nullability has been tested before
|
||||||
val eventId = event.root.eventId!!
|
val eventId = event.root.eventId!!
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
|
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
val displayReadMarker = event.displayReadMarker(session.myUserId)
|
val displayReadMarker = !hideReadMarker && event.displayReadMarker(session.myUserId)
|
||||||
|
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
@ -91,6 +91,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
|
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
|
||||||
}
|
}
|
||||||
.toList(),
|
.toList(),
|
||||||
|
hasReadMarker = event.hasReadMarker,
|
||||||
displayReadMarker = displayReadMarker
|
displayReadMarker = displayReadMarker
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -56,8 +56,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
||||||
override fun onReadMarkerDisplayed() {
|
|
||||||
attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData)
|
override fun onReadMarkerLongBound() {
|
||||||
|
attributes.readReceiptsCallback?.onReadMarkerLongDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +107,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
holder.memberNameView.setOnLongClickListener(null)
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
}
|
}
|
||||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||||
holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback)
|
holder.readMarkerView.bindView(
|
||||||
|
attributes.informationData.eventId,
|
||||||
|
attributes.informationData.hasReadMarker,
|
||||||
|
attributes.informationData.displayReadMarker,
|
||||||
|
_readMarkerCallback
|
||||||
|
)
|
||||||
|
|
||||||
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
|
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
|
||||||
holder.reactionWrapper?.isVisible = false
|
holder.reactionWrapper?.isVisible = false
|
||||||
|
@ -25,7 +25,9 @@ import androidx.core.view.isVisible
|
|||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.ui.views.ReadMarkerView
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
||||||
@ -37,6 +39,13 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
attributes.mergeData.distinctBy { it.userId }
|
attributes.mergeData.distinctBy { it.userId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
||||||
|
|
||||||
|
override fun onReadMarkerLongBound() {
|
||||||
|
attributes.readReceiptsCallback?.onReadMarkerLongDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
@ -68,6 +77,16 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
}
|
}
|
||||||
// No read receipt for this item
|
// No read receipt for this item
|
||||||
holder.readReceiptsView.isVisible = false
|
holder.readReceiptsView.isVisible = false
|
||||||
|
holder.readMarkerView.bindView(
|
||||||
|
attributes.readMarkerId,
|
||||||
|
!attributes.readMarkerId.isNullOrEmpty(),
|
||||||
|
attributes.showReadMarker,
|
||||||
|
_readMarkerCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unbind(holder: Holder) {
|
||||||
|
holder.readMarkerView.unbind()
|
||||||
|
super.unbind(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@ -78,10 +97,12 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
|
val readMarkerId: String?,
|
||||||
val isCollapsed: Boolean,
|
val isCollapsed: Boolean,
|
||||||
val showReadMarker: Boolean,
|
val showReadMarker: Boolean,
|
||||||
val mergeData: List<Data>,
|
val mergeData: List<Data>,
|
||||||
val avatarRenderer: AvatarRenderer,
|
val avatarRenderer: AvatarRenderer,
|
||||||
|
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ data class MessageInformationData(
|
|||||||
val hasBeenEdited: Boolean = false,
|
val hasBeenEdited: Boolean = false,
|
||||||
val hasPendingEdits: Boolean = false,
|
val hasPendingEdits: Boolean = false,
|
||||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||||
|
val hasReadMarker: Boolean = false,
|
||||||
val displayReadMarker: Boolean = false
|
val displayReadMarker: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@ -38,8 +38,8 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
private val _readMarkerCallback = object : ReadMarkerView.Callback {
|
||||||
override fun onReadMarkerDisplayed() {
|
override fun onReadMarkerLongBound() {
|
||||||
attributes.readReceiptsCallback?.onReadMarkerLongDisplayed(attributes.informationData)
|
attributes.readReceiptsCallback?.onReadMarkerLongDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,12 +50,17 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||||||
attributes.informationData.avatarUrl,
|
attributes.informationData.avatarUrl,
|
||||||
attributes.informationData.senderId,
|
attributes.informationData.senderId,
|
||||||
attributes.informationData.memberName?.toString()
|
attributes.informationData.memberName?.toString()
|
||||||
?: attributes.informationData.senderId,
|
?: attributes.informationData.senderId,
|
||||||
holder.avatarImageView
|
holder.avatarImageView
|
||||||
)
|
)
|
||||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||||
holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback)
|
holder.readMarkerView.bindView(
|
||||||
|
attributes.informationData.eventId,
|
||||||
|
attributes.informationData.hasReadMarker,
|
||||||
|
attributes.informationData.displayReadMarker,
|
||||||
|
_readMarkerCallback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user