forked from GitHub-Mirror/riotX-android
Compare commits
2 Commits
develop
...
feature/re
Author | SHA1 | Date | |
---|---|---|---|
|
51a4c93676 | ||
|
d8f449388c |
@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
|
||||
|
||||
|
||||
vector.debugPrivateData=false
|
||||
vector.httpLogLevel=NONE
|
||||
vector.httpLogLevel=HEADERS
|
||||
|
||||
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
|
||||
#vector.debugPrivateData=true
|
||||
|
@ -35,9 +35,14 @@ data class RoomSummary(
|
||||
val highlightCount: Int = 0,
|
||||
val tags: List<RoomTag> = 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
|
||||
}
|
||||
|
||||
val hasNewMessages: Boolean
|
||||
get() = notificationCount != 0
|
||||
}
|
||||
|
||||
|
@ -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.matrix.android.api.session.room.read
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FullyReadContent(
|
||||
@Json(name = "event_id") val eventId: String
|
||||
)
|
@ -42,5 +42,10 @@ interface ReadService {
|
||||
|
||||
fun isEventRead(eventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns a nullable read marker for the room.
|
||||
*/
|
||||
fun getReadMarkerLive(): LiveData<String?>
|
||||
|
||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||
}
|
@ -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 {
|
||||
/**
|
||||
|
@ -39,7 +39,8 @@ data class TimelineEvent(
|
||||
val isUniqueDisplayName: Boolean,
|
||||
val senderAvatar: String?,
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
val readReceipts: List<ReadReceipt> = emptyList(),
|
||||
val hasReadMarker: Boolean = false
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
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.ReadReceiptsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
@ -157,7 +158,6 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val eventEntity = TimelineEventEntity(localId).also {
|
||||
it.root = event.toEntity(roomId).apply {
|
||||
this.stateIndex = currentStateIndex
|
||||
@ -169,6 +169,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
it.roomId = roomId
|
||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
it.readReceipts = readReceiptsSummaryEntity
|
||||
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
|
||||
}
|
||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||
timelineEvents.add(position, eventEntity)
|
||||
|
@ -26,8 +26,8 @@ import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomSummaryMapper @Inject constructor(
|
||||
val cryptoService: CryptoService,
|
||||
val timelineEventMapper: TimelineEventMapper
|
||||
private val cryptoService: CryptoService,
|
||||
private val timelineEventMapper: TimelineEventMapper
|
||||
) {
|
||||
|
||||
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,8 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
readReceipts = readReceipts?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList()
|
||||
} ?: emptyList(),
|
||||
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isEmpty() == false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class ReadMarkerEntity(
|
||||
@PrimaryKey
|
||||
var roomId: String = "",
|
||||
var eventId: String = ""
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("readMarker")
|
||||
val timelineEvent: RealmResults<TimelineEventEntity>? = null
|
||||
|
||||
companion object
|
||||
|
||||
}
|
@ -35,7 +35,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
|
||||
var otherMemberIds: RealmList<String> = RealmList(),
|
||||
var notificationCount: Int = 0,
|
||||
var highlightCount: Int = 0,
|
||||
var tags: RealmList<RoomTagEntity> = RealmList()
|
||||
var tags: RealmList<RoomTagEntity> = RealmList(),
|
||||
var readMarkerId: String? = null
|
||||
) : RealmObject() {
|
||||
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
|
@ -43,6 +43,7 @@ import io.realm.annotations.RealmModule
|
||||
PushConditionEntity::class,
|
||||
PusherEntity::class,
|
||||
PusherDataEntity::class,
|
||||
ReadReceiptsSummaryEntity::class
|
||||
ReadReceiptsSummaryEntity::class,
|
||||
ReadMarkerEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
@ -31,7 +31,8 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
||||
var isUniqueDisplayName: Boolean = false,
|
||||
var senderAvatar: String? = null,
|
||||
var senderMembershipEvent: EventEntity? = null,
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null,
|
||||
var readMarker: ReadMarkerEntity? = null
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("timelineEvents")
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.database.query
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
|
||||
val query = realm.where<ReadMarkerEntity>()
|
||||
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
|
||||
if (eventId != null) {
|
||||
query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
|
||||
return where(realm, roomId).findFirst()
|
||||
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
|
||||
}
|
@ -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<ReadReceiptEntity> {
|
||||
@ -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<ReadReceiptEntity> {
|
||||
return realm.where<ReadReceiptEntity>()
|
||||
.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"
|
||||
|
@ -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<RoomSummaryEntity> {
|
||||
return RoomSummaryEntity.where(realm)
|
||||
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
|
||||
|
@ -21,4 +21,4 @@ import javax.inject.Scope
|
||||
@Scope
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class MatrixScope
|
||||
internal annotation class MatrixScope
|
@ -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
|
||||
|
@ -21,4 +21,4 @@ import javax.inject.Scope
|
||||
@Scope
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SessionScope
|
||||
internal annotation class SessionScope
|
@ -28,8 +28,10 @@ import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.internal.database.RealmLiveData
|
||||
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.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
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.where
|
||||
@ -93,6 +95,15 @@ internal class DefaultReadService @AssistedInject constructor(@Assisted private
|
||||
return isEventRead
|
||||
}
|
||||
|
||||
override fun getReadMarkerLive(): LiveData<String?> {
|
||||
val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||
ReadMarkerEntity.where(realm, roomId)
|
||||
}
|
||||
return Transformations.map(liveRealmData) { results ->
|
||||
results.firstOrNull()?.eventId
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
|
||||
val liveEntity = RealmLiveData(monarchy.realmConfiguration) { realm ->
|
||||
ReadReceiptsSummaryEntity.where(realm, eventId)
|
||||
|
@ -18,19 +18,25 @@ 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
|
||||
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
|
||||
|
||||
@ -49,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) {
|
||||
@ -57,6 +64,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
val fullyReadEventId: String?
|
||||
val readReceiptEventId: String?
|
||||
|
||||
Timber.v("Execute set read marker with params: $params")
|
||||
if (params.markAllAsRead) {
|
||||
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
|
||||
@ -68,16 +76,16 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
readReceiptEventId = params.readReceiptEventId
|
||||
}
|
||||
|
||||
if (fullyReadEventId != null) {
|
||||
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
|
||||
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)) {
|
||||
|
||||
&& !isEventRead(params.roomId, readReceiptEventId)) {
|
||||
if (LocalEchoEventFactory.isLocalEchoId(readReceiptEventId)) {
|
||||
Timber.w("Can't set read receipt for local event ${params.fullyReadEventId}")
|
||||
} else {
|
||||
@ -93,12 +101,30 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
|
||||
monarchy.writeAsync { realm ->
|
||||
|
||||
private fun isReadMarkerMoreRecent(roomId: String, fullyReadEventId: String): Boolean {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
val readMarkerEntity = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()
|
||||
val readMarkerEvent = readMarkerEntity?.timelineEvent?.firstOrNull()
|
||||
val eventToCheck = TimelineEventEntity.where(realm, eventId = fullyReadEventId).findFirst()
|
||||
val readReceiptIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
|
||||
val eventToCheckIndex = eventToCheck?.root?.displayIndex ?: Int.MIN_VALUE
|
||||
eventToCheckIndex > readReceiptIndex
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -106,19 +132,17 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
|
||||
}
|
||||
|
||||
private fun isEventRead(roomId: String, eventId: String): Boolean {
|
||||
var isEventRead = false
|
||||
monarchy.doWithRealm {
|
||||
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
||||
?: return@doWithRealm
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
||||
?: return@doWithRealm
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, credentials.userId).findFirst()
|
||||
?: return false
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
?: return false
|
||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||
?: Int.MIN_VALUE
|
||||
?: Int.MIN_VALUE
|
||||
val eventToCheckIndex = liveChunk.timelineEvents.find(eventId)?.root?.displayIndex
|
||||
?: Int.MAX_VALUE
|
||||
isEventRead = eventToCheckIndex <= readReceiptIndex
|
||||
?: Int.MAX_VALUE
|
||||
eventToCheckIndex <= readReceiptIndex
|
||||
}
|
||||
return isEventRead
|
||||
}
|
||||
|
||||
}
|
@ -27,33 +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.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.OrderedCollectionChangeSet
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import io.realm.*
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@ -67,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,
|
||||
@ -75,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,7 +87,6 @@ internal class DefaultTimeline(
|
||||
|
||||
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private val isLive = initialEventId == null
|
||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||
@ -112,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<RealmResults<TimelineEventEntity>> { results, changeSet ->
|
||||
@ -120,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) {
|
||||
@ -149,13 +131,9 @@ internal class DefaultTimeline(
|
||||
changeSet.changes.forEach { index ->
|
||||
val eventEntity = results[index]
|
||||
eventEntity?.eventId?.let { eventId ->
|
||||
builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update an existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = buildTimelineEvent(eventEntity)
|
||||
hasChanged = true
|
||||
}
|
||||
}
|
||||
hasChanged = rebuildEvent(eventId) {
|
||||
buildTimelineEvent(eventEntity)
|
||||
} || hasChanged
|
||||
}
|
||||
}
|
||||
if (hasChanged) postSnapshot()
|
||||
@ -163,27 +141,21 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
|
||||
|
||||
var hasChange = false
|
||||
|
||||
(changeSet.insertions + changeSet.changes).forEach {
|
||||
val eventRelations = collection[it]
|
||||
if (eventRelations != null) {
|
||||
builtEventsIdMap[eventRelations.eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain())
|
||||
hasChange = true
|
||||
}
|
||||
}
|
||||
hasChange = rebuildEvent(eventRelations.eventId) { te ->
|
||||
te.copy(annotations = eventRelations.asDomain())
|
||||
} || hasChange
|
||||
}
|
||||
}
|
||||
if (hasChange)
|
||||
postSnapshot()
|
||||
if (hasChange) postSnapshot()
|
||||
}
|
||||
|
||||
|
||||
// Public methods ******************************************************************************
|
||||
// Public methods ******************************************************************************
|
||||
|
||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||
BACKGROUND_HANDLER.post {
|
||||
@ -240,7 +212,7 @@ internal class DefaultTimeline(
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.start(realm, liveEvents, this)
|
||||
}
|
||||
|
||||
hiddenReadMarker.start(realm, liveEvents, this)
|
||||
isReady.set(true)
|
||||
}
|
||||
}
|
||||
@ -255,9 +227,11 @@ internal class DefaultTimeline(
|
||||
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
||||
eventRelations.removeAllChangeListeners()
|
||||
liveEvents.removeAllChangeListeners()
|
||||
hiddenReadMarker.dispose()
|
||||
if (settings.buildReadReceipts) {
|
||||
hiddenReadReceipts.dispose()
|
||||
}
|
||||
clearAllValues()
|
||||
backgroundRealm.getAndSet(null).also {
|
||||
it.close()
|
||||
}
|
||||
@ -265,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)
|
||||
}
|
||||
@ -272,20 +267,38 @@ internal class DefaultTimeline(
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = te.copy(readReceipts = readReceipts)
|
||||
true
|
||||
}
|
||||
} ?: false
|
||||
return rebuildEvent(eventId) { te ->
|
||||
te.copy(readReceipts = readReceipts)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReadReceiptsUpdated() {
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
// 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 {
|
||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||
//Update the relation of existing event
|
||||
builtEvents[builtIndex]?.let { te ->
|
||||
builtEvents[builtIndex] = builder(te)
|
||||
true
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
||||
@ -395,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) {
|
||||
@ -543,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,12 +581,24 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Extension methods ***************************************************************************
|
||||
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 {
|
||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||
|
@ -59,7 +59,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
cryptoService,
|
||||
timelineEventMapper,
|
||||
settings,
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||
TimelineHiddenReadMarker(roomId)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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<TimelineEventEntity>
|
||||
private lateinit var delegate: Delegate
|
||||
|
||||
private val readMarkerListener = RealmObjectChangeListener<ReadMarkerEntity> { 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<TimelineEventEntity>, 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()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.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
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomFullyReadHandler @Inject constructor() {
|
||||
|
||||
fun handle(realm: Realm, roomId: String, content: FullyReadContent?) {
|
||||
if (content == null) {
|
||||
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
|
||||
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
|
||||
timelineEventEntity?.readMarker = readMarkerEntity
|
||||
}
|
||||
|
||||
}
|
@ -23,8 +23,13 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
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.crypto.CryptoManager
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
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.addStateEvent
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.helper.updateSenderDataFor
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
@ -37,7 +42,11 @@ import im.vector.matrix.android.internal.session.notification.DefaultPushRuleSer
|
||||
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
@ -50,6 +59,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||
private val roomTagHandler: RoomTagHandler,
|
||||
private val roomFullyReadHandler: RoomFullyReadHandler,
|
||||
private val cryptoManager: CryptoManager,
|
||||
private val tokenStore: SyncTokenStore,
|
||||
private val pushRuleService: DefaultPushRuleService,
|
||||
@ -247,11 +257,16 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
accountData.events
|
||||
.asSequence()
|
||||
.filter { it.getClearType() == EventType.TAG }
|
||||
.map { it.content.toModel<RoomTagContent>() }
|
||||
.forEach { roomTagHandler.handle(realm, roomId, it) }
|
||||
for (event in accountData.events) {
|
||||
val eventType = event.getClearType()
|
||||
if (eventType == EventType.TAG) {
|
||||
val content = event.getClearContent().toModel<RoomTagContent>()
|
||||
roomTagHandler.handle(realm, roomId, content)
|
||||
} else if (eventType == EventType.FULLY_READ) {
|
||||
val content = event.getClearContent().toModel<FullyReadContent>()
|
||||
roomFullyReadHandler.handle(realm, roomId, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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?
|
||||
)
|
@ -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<MediaFile>) : 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()
|
||||
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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<RoomDetailActions.EventDisplayed>()
|
||||
private val invisibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsInvisible>()
|
||||
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailActions.TimelineEventTurnsVisible>()
|
||||
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<RoomTombstoneContent>()
|
||||
?: 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,7 +591,7 @@ 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 ->
|
||||
@ -634,6 +603,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
.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<Unit> {})
|
||||
}
|
||||
|
||||
private fun handleMarkAllAsRead() {
|
||||
room.markAllAsRead(object : MatrixCallback<Any> {})
|
||||
}
|
||||
|
||||
private fun observeSyncState() {
|
||||
session.rx()
|
||||
.liveSyncState()
|
||||
@ -645,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, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { 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<Boolean> {
|
||||
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 ->
|
||||
|
@ -51,7 +51,9 @@ data class RoomDetailViewState(
|
||||
val isEncrypted: Boolean = false,
|
||||
val tombstoneEvent: Event? = null,
|
||||
val tombstoneEventHandling: Async<String> = 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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<ReadReceiptData>)
|
||||
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<TimelineEvent>): 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
|
||||
)
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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<EncryptionEventContent>() ?: return null
|
||||
stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<EventHtmlRenderer>,
|
||||
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<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
|
||||
return noticeItemFactory.create(event, highlight, callback)
|
||||
}
|
||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||
|
||||
// val all = event.root.toContent()
|
||||
// val ev = all.toModel<Event>()
|
||||
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 {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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<RoomHistoryVisibilityContent>()?.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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,6 +64,9 @@ 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
|
||||
|
||||
return MessageInformationData(
|
||||
eventId = eventId,
|
||||
senderId = event.root.senderId ?: "",
|
||||
@ -85,7 +90,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
.map {
|
||||
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
|
||||
}
|
||||
.toList()
|
||||
.toList(),
|
||||
displayReadMarker = displayReadMarker
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<MessageContent>()?.relatesTo?.type == RelationType.REPLACE) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
//
|
||||
//fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
// 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<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<Timel
|
||||
.reversed()
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int, showHiddenEvent: Boolean): TimelineEvent? {
|
||||
fun List<TimelineEvent>.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()
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
|
||||
@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<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
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,10 +108,10 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
holder.avatarImageView.setOnLongClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
}
|
||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||
holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback)
|
||||
|
||||
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
|
||||
@ -143,7 +123,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
//clear all reaction buttons (but not the Flow helper!)
|
||||
holder.reactionWrapper?.children?.forEach { (it as? ReactionButton)?.isGone = true }
|
||||
val idToRefInFlow = ArrayList<Int>()
|
||||
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
|
||||
@ -151,7 +131,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
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
|
||||
}
|
||||
@ -162,26 +142,48 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||
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<ImageView>(R.id.messageAvatarImageView)
|
||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
||||
var reactionWrapper: ViewGroup? = null
|
||||
var reactionFlowHelper: Flow? = null
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -43,21 +43,21 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
||||
if (!informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData, holder.progressLayout)
|
||||
}
|
||||
holder.imageView.setOnClickListener(clickListener)
|
||||
holder.imageView.setOnLongClickListener(longClickListener)
|
||||
holder.imageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
|
||||
holder.mediaContentView.setOnClickListener(cellClickListener)
|
||||
holder.mediaContentView.setOnLongClickListener(longClickListener)
|
||||
holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
|
||||
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
// The sending state color will be apply to the progress text
|
||||
renderSendState(holder.imageView, null, holder.failedToSendIndicator)
|
||||
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
contentUploadStateTrackerBinder.unbind(informationData.eventId)
|
||||
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,8 @@ data class MessageInformationData(
|
||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||
val hasBeenEdited: Boolean = false,
|
||||
val hasPendingEdits: Boolean = false,
|
||||
val readReceipts: List<ReadReceiptData> = emptyList()
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val displayReadMarker: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
|
||||
|
@ -79,8 +79,8 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
|
||||
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) }
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import android.widget.TextView
|
||||
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
|
||||
@ -53,6 +54,12 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
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
|
||||
@ -60,11 +67,17 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
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.bindView(informationData, _readMarkerCallback)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
holder.readMarkerView.unbind()
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
@ -73,6 +86,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<scale
|
||||
android:duration="1500"
|
||||
android:fromXScale="1"
|
||||
android:fromYScale="1"
|
||||
android:pivotX="50%p"
|
||||
@ -10,7 +9,6 @@
|
||||
android:toYScale="0" />
|
||||
|
||||
<alpha
|
||||
android:duration="1500"
|
||||
android:fromAlpha="1"
|
||||
android:toAlpha="0" />
|
||||
</set>
|
@ -6,6 +6,29 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/syncProgressBarWrap"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/syncProgressBar"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?riotx_header_panel_background"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Trick to remove surrounding padding (clip frome wrapping frame) -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/roomToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
@ -71,28 +94,12 @@
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<!-- Trick to remove surrounding padding (clip frome wrapping frame) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/syncProgressBarWrap"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="3dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/syncProgressBar"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?riotx_header_panel_background"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/recyclerViewBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
||||
|
||||
<com.airbnb.epoxy.EpoxyRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
@ -105,22 +112,14 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap"
|
||||
tools:listitem="@layout/item_timeline_event_base" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/recyclerViewBarrier"
|
||||
android:layout_width="wrap_content"
|
||||
<im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
||||
android:id="@+id/jumpToReadMarkerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
|
||||
|
||||
<im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
||||
android:id="@+id/composerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?riotx_background"
|
||||
android:transitionName="composer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/syncProgressBarWrap" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.NotificationAreaView
|
||||
android:id="@+id/notificationAreaView"
|
||||
@ -132,6 +131,16 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
||||
android:id="@+id/composerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?riotx_background"
|
||||
android:transitionName="composer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<im.vector.riotx.features.invite.VectorInviteView
|
||||
android:id="@+id/inviteView"
|
||||
android:layout_width="0dp"
|
||||
@ -144,5 +153,4 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -122,15 +122,26 @@
|
||||
|
||||
</ViewStub>
|
||||
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
android:id="@+id/readReceiptsView"
|
||||
android:layout_width="wrap_content"
|
||||
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" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
android:id="@+id/readMarkerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp"
|
||||
android:background="?attr/vctr_unread_marker_line_color"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:visibility="invisible"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -58,8 +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" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadMarkerView
|
||||
android:id="@+id/readMarkerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="2dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="?attr/vctr_unread_marker_line_color"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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" />
|
||||
|
41
vector/src/main/res/layout/view_jump_to_read_marker.xml
Normal file
41
vector/src/main/res/layout/view_jump_to_read_marker.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/notification_accent_color"
|
||||
tools:parentTag="android.widget.RelativeLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/jumpToReadMarkerLabelView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_toStartOf="@+id/closeJumpToReadMarkerView"
|
||||
android:layout_toLeftOf="@+id/closeJumpToReadMarkerView"
|
||||
android:drawableStart="@drawable/jump_to_unread"
|
||||
android:drawableLeft="@drawable/jump_to_unread"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/room_jump_to_first_unread"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/closeJumpToReadMarkerView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignTop="@+id/jumpToReadMarkerLabelView"
|
||||
android:layout_alignBottom="@+id/jumpToReadMarkerLabelView"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:contentDescription="@string/action_close"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:src="@drawable/ic_clear_white" />
|
||||
|
||||
</merge>
|
58
vector/src/main/res/layout/view_read_marker.xml
Normal file
58
vector/src/main/res/layout/view_read_marker.xml
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:parentTag="android.widget.LinearLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/receiptMore"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center"
|
||||
android:textSize="12sp"
|
||||
tools:text="999+" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar5"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar4"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar3"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar2"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/receiptAvatar1"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</merge>
|
Loading…
Reference in New Issue
Block a user