mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-16 02:05:06 +08:00
- 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
|
||||
* @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
|
||||
* 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
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
override suspend fun fetchThreadTimeline(rootThreadEventId: String): List<Event> {
|
||||
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
|
||||
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
|
||||
}
|
||||
|
||||
|
@ -15,17 +15,45 @@
|
||||
*/
|
||||
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.EventType
|
||||
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.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.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.timeline.PaginationDirection
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, List<Event>> {
|
||||
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val rootThreadEventId: String
|
||||
@ -35,10 +63,13 @@ internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params
|
||||
internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
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 {
|
||||
|
||||
override suspend fun execute(params: FetchThreadTimelineTask.Params): List<Event> {
|
||||
override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
|
||||
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
||||
val response = executeRequest(globalErrorReceiver) {
|
||||
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()
|
||||
return copy(
|
||||
readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback),
|
||||
readReceiptsItem = readReceiptsItemFactory.create(
|
||||
event.eventId,
|
||||
readReceipts,
|
||||
callback,
|
||||
partialState.isFromThreadTimeline()
|
||||
),
|
||||
formattedDayModel = formattedDayModel,
|
||||
mergedHeaderModel = mergedHeaderModel
|
||||
)
|
||||
|
@ -26,7 +26,11 @@ import javax.inject.Inject
|
||||
|
||||
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()) {
|
||||
return null
|
||||
}
|
||||
@ -41,6 +45,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||
.eventId(eventId)
|
||||
.readReceipts(readReceiptsData)
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.shouldHideReadReceipts(isFromThreadTimeLine)
|
||||
.clickListener {
|
||||
callback?.onReadReceiptsClicked(readReceiptsData)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.airbnb.epoxy.EpoxyModelWithHolder
|
||||
@ -31,6 +32,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
||||
|
||||
@EpoxyAttribute lateinit var eventId: String
|
||||
@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 clickListener: ClickListener
|
||||
|
||||
@ -42,6 +44,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
||||
super.bind(holder)
|
||||
holder.readReceiptsView.onClick(clickListener)
|
||||
holder.readReceiptsView.render(readReceipts, avatarRenderer)
|
||||
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
|
Loading…
Reference in New Issue
Block a user