- Hide read receipts from thread timeline
- Enhance FetchThreadTimelineTask
This commit is contained in:
parent
707397cb9d
commit
4cff3938e7
|
@ -153,5 +153,5 @@ interface RelationService {
|
||||||
* from the backend
|
* from the backend
|
||||||
* @param rootThreadEventId the root thread eventId
|
* @param rootThreadEventId the root thread eventId
|
||||||
*/
|
*/
|
||||||
suspend fun fetchThreadTimeline(rootThreadEventId: String): List<Event>
|
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,10 @@ import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||||
* Finds the root thread event and update it with the latest message summary along with the number
|
* Finds the root thread event and update it with the latest message summary along with the number
|
||||||
* of threads included. If there is no root thread event no action is done
|
* of threads included. If there is no root thread event no action is done
|
||||||
*/
|
*/
|
||||||
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(roomId: String, realm: Realm, currentUserId: String) {
|
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
|
||||||
|
roomId: String,
|
||||||
|
realm: Realm, currentUserId: String,
|
||||||
|
shouldUpdateNotifications: Boolean = true) {
|
||||||
if (!BuildConfig.THREADING_ENABLED) return
|
if (!BuildConfig.THREADING_ENABLED) return
|
||||||
|
|
||||||
for ((rootThreadEventId, eventEntity) in this) {
|
for ((rootThreadEventId, eventEntity) in this) {
|
||||||
|
@ -55,7 +58,9 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(roomId: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNotificationsNew(roomId, realm, currentUserId)
|
if(shouldUpdateNotifications) {
|
||||||
|
updateNotificationsNew(roomId, realm, currentUserId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -203,7 +203,7 @@ internal class DefaultRelationService @AssistedInject constructor(
|
||||||
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchThreadTimeline(rootThreadEventId: String): List<Event> {
|
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
|
||||||
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
|
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,17 +15,45 @@
|
||||||
*/
|
*/
|
||||||
package org.matrix.android.sdk.internal.session.room.relation.threads
|
package org.matrix.android.sdk.internal.session.room.relation.threads
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import io.realm.Realm
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
|
||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
|
||||||
|
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
|
||||||
|
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||||
|
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||||
|
import org.matrix.android.sdk.internal.database.mapper.toEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
|
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||||
|
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
|
||||||
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
|
import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||||
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
import org.matrix.android.sdk.internal.network.executeRequest
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
|
||||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||||
|
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, List<Event>> {
|
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val rootThreadEventId: String
|
val rootThreadEventId: String
|
||||||
|
@ -35,10 +63,13 @@ internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params
|
||||||
internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
||||||
private val roomAPI: RoomAPI,
|
private val roomAPI: RoomAPI,
|
||||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
|
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
|
||||||
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
|
@UserId private val userId: String,
|
||||||
|
private val cryptoService: DefaultCryptoService
|
||||||
) : FetchThreadTimelineTask {
|
) : FetchThreadTimelineTask {
|
||||||
|
|
||||||
override suspend fun execute(params: FetchThreadTimelineTask.Params): List<Event> {
|
override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
|
||||||
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
||||||
val response = executeRequest(globalErrorReceiver) {
|
val response = executeRequest(globalErrorReceiver) {
|
||||||
roomAPI.getRelations(
|
roomAPI.getRelations(
|
||||||
|
@ -50,6 +81,132 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.chunks + listOfNotNull(response.originalEvent)
|
val threadList = response.chunks + listOfNotNull(response.originalEvent)
|
||||||
|
|
||||||
|
|
||||||
|
return storeNewEventsIfNeeded(threadList, params.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store new events if they are not already received, and returns weather or not,
|
||||||
|
* a timeline update should be made
|
||||||
|
* @param threadList is the list containing the thread replies
|
||||||
|
* @param roomId the roomId of the the thread
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
|
||||||
|
var eventsSkipped = 0
|
||||||
|
monarchy
|
||||||
|
.awaitTransaction { realm ->
|
||||||
|
val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
|
||||||
|
|
||||||
|
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||||
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
|
||||||
|
for (event in threadList.reversed()) {
|
||||||
|
|
||||||
|
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||||
|
eventsSkipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EventEntity.where(realm, event.eventId).findFirst() != null) {
|
||||||
|
// Skip if event already exists
|
||||||
|
eventsSkipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (event.isEncrypted()) {
|
||||||
|
// Decrypt events that will be stored
|
||||||
|
decryptIfNeeded(event, roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReaction(realm, event, roomId)
|
||||||
|
|
||||||
|
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
|
||||||
|
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
|
||||||
|
|
||||||
|
// Sender info
|
||||||
|
roomMemberContentsByUser.getOrPut(event.senderId) {
|
||||||
|
// If we don't have any new state on this user, get it from db
|
||||||
|
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
|
||||||
|
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||||
|
eventEntity.rootThreadEventId?.let {
|
||||||
|
// This is a thread event
|
||||||
|
optimizedThreadSummaryMap[it] = eventEntity
|
||||||
|
} ?: run {
|
||||||
|
// This is a normal event or a root thread one
|
||||||
|
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
|
||||||
|
roomId = roomId,
|
||||||
|
realm = realm,
|
||||||
|
currentUserId = userId,
|
||||||
|
shouldUpdateNotifications = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
|
||||||
|
|
||||||
|
return eventsSkipped == threadList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoke the event decryption mechanism for a specific event
|
||||||
|
*/
|
||||||
|
|
||||||
|
private fun decryptIfNeeded(event: Event, roomId: String) {
|
||||||
|
try {
|
||||||
|
// Event from sync does not have roomId, so add it to the event first
|
||||||
|
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
|
||||||
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
} catch (e: MXCryptoError) {
|
||||||
|
if (e is MXCryptoError.Base) {
|
||||||
|
event.mCryptoError = e.errorType
|
||||||
|
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReaction(realm: Realm,
|
||||||
|
event: Event,
|
||||||
|
roomId: String) {
|
||||||
|
|
||||||
|
val unsignedData = event.unsignedData ?: return
|
||||||
|
val relatedEventId = event.eventId ?: return
|
||||||
|
|
||||||
|
unsignedData.relations?.annotations?.chunk?.forEach { relationChunk ->
|
||||||
|
|
||||||
|
if (relationChunk.type == EventType.REACTION) {
|
||||||
|
val reaction = relationChunk.key
|
||||||
|
Timber.i("----> Annotation found in ${event.eventId} ${relationChunk.key} ")
|
||||||
|
|
||||||
|
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
|
||||||
|
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||||
|
|
||||||
|
if (sum == null) {
|
||||||
|
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||||
|
sum.key = reaction
|
||||||
|
sum.firstTimestamp = event.originServerTs ?: 0
|
||||||
|
Timber.v("Adding synced reaction $reaction")
|
||||||
|
sum.count = 1
|
||||||
|
// reactionEventId not included in the /relations API
|
||||||
|
// sum.sourceEvents.add(reactionEventId)
|
||||||
|
eventSummary.reactionsSummary.add(sum)
|
||||||
|
} else {
|
||||||
|
sum.count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -443,7 +443,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
}
|
}
|
||||||
val readReceipts = receiptsByEvents[event.eventId].orEmpty()
|
val readReceipts = receiptsByEvents[event.eventId].orEmpty()
|
||||||
return copy(
|
return copy(
|
||||||
readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback),
|
readReceiptsItem = readReceiptsItemFactory.create(
|
||||||
|
event.eventId,
|
||||||
|
readReceipts,
|
||||||
|
callback,
|
||||||
|
partialState.isFromThreadTimeline()
|
||||||
|
),
|
||||||
formattedDayModel = formattedDayModel,
|
formattedDayModel = formattedDayModel,
|
||||||
mergedHeaderModel = mergedHeaderModel
|
mergedHeaderModel = mergedHeaderModel
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,7 +26,11 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
|
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
|
||||||
|
|
||||||
fun create(eventId: String, readReceipts: List<ReadReceipt>, callback: TimelineEventController.Callback?): ReadReceiptsItem? {
|
fun create(
|
||||||
|
eventId: String,
|
||||||
|
readReceipts: List<ReadReceipt>,
|
||||||
|
callback: TimelineEventController.Callback?,
|
||||||
|
isFromThreadTimeLine: Boolean): ReadReceiptsItem? {
|
||||||
if (readReceipts.isEmpty()) {
|
if (readReceipts.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -41,6 +45,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||||
.eventId(eventId)
|
.eventId(eventId)
|
||||||
.readReceipts(readReceiptsData)
|
.readReceipts(readReceiptsData)
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.shouldHideReadReceipts(isFromThreadTimeLine)
|
||||||
.clickListener {
|
.clickListener {
|
||||||
callback?.onReadReceiptsClicked(readReceiptsData)
|
callback?.onReadReceiptsClicked(readReceiptsData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||||
|
@ -31,6 +32,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var eventId: String
|
@EpoxyAttribute lateinit var eventId: String
|
||||||
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
|
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
|
||||||
|
@EpoxyAttribute var shouldHideReadReceipts: Boolean = false
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
|
||||||
|
|
||||||
|
@ -42,6 +44,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.readReceiptsView.onClick(clickListener)
|
holder.readReceiptsView.onClick(clickListener)
|
||||||
holder.readReceiptsView.render(readReceipts, avatarRenderer)
|
holder.readReceiptsView.render(readReceipts, avatarRenderer)
|
||||||
|
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
|
|
Loading…
Reference in New Issue