mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 04:08:44 +03:00
- Enhance local notification to work with read receipt & the latest chunk
- Local notification mentioning system - Fix/Improve thread list filtering
This commit is contained in:
parent
5c015a7444
commit
d56281dca7
20 changed files with 318 additions and 63 deletions
|
@ -84,7 +84,7 @@ interface TimelineService {
|
|||
* Returns whether or not the current user is participating in the thread
|
||||
* @param rootThreadEventId the eventId of the current thread
|
||||
*/
|
||||
fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean
|
||||
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
|
||||
|
||||
/**
|
||||
* Marks the current thread as read. This is a local implementation
|
||||
|
|
|
@ -18,10 +18,14 @@ package org.matrix.android.sdk.api.session.threads
|
|||
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
|
||||
/**
|
||||
* This class contains all the details needed for threads.
|
||||
* Is is mainly used from within an Event.
|
||||
*/
|
||||
data class ThreadDetails(
|
||||
val isRootThread: Boolean = false,
|
||||
val numberOfThreads: Int = 0,
|
||||
val threadSummarySenderInfo: SenderInfo? = null,
|
||||
val threadSummaryLatestTextMessage: String? = null,
|
||||
val hasUnreadMessage: Boolean = false
|
||||
var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
|
||||
)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.api.session.threads
|
||||
|
||||
/**
|
||||
* This class defines the state of a thread notification badge
|
||||
*/
|
||||
data class ThreadNotificationBadgeState(
|
||||
val numberOfLocalUnreadThreads: Int = 0,
|
||||
val isUserMentioned: Boolean = false
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.api.session.threads
|
||||
|
||||
/**
|
||||
* This class defines the state of a thread notification
|
||||
*/
|
||||
enum class ThreadNotificationState {
|
||||
|
||||
// There are no new message
|
||||
NO_NEW_MESSAGE,
|
||||
|
||||
// There is at least one new message
|
||||
NEW_MESSAGE,
|
||||
|
||||
// The is at least one new message that should bi highlighted
|
||||
// ex. "Hello @aris.kotsomitopoulos"
|
||||
NEW_HIGHLIGHTED_MESSAGE;
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.api.session.threads
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
||||
/**
|
||||
* This class contains a thread TimelineEvent along with a boolean that
|
||||
* determines if the current user has participated in that event
|
||||
*/
|
||||
data class ThreadTimelineEvent(
|
||||
val timelineEvent: TimelineEvent,
|
||||
val isParticipating: Boolean
|
||||
)
|
|
@ -375,7 +375,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
|
|||
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
|
||||
?.addField(EventEntityFields.HAS_UNREAD_THREAD_MESSAGES, Boolean::class.java)
|
||||
?.addField(EventEntityFields.THREAD_NOTIFICATION_STATE_STR, String::class.java)
|
||||
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,14 @@ import io.realm.RealmQuery
|
|||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.database.mapper.asDomain
|
||||
import org.matrix.android.sdk.internal.database.model.ChunkEntity
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.findIncludingEvent
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.database.query.whereRoomId
|
||||
|
||||
|
@ -31,7 +36,7 @@ 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(isInitialSync: Boolean = false, currentUserId: String? = null) {
|
||||
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(roomId: String, realm: Realm, currentUserId: String) {
|
||||
|
||||
if (!BuildConfig.THREADING_ENABLED) return
|
||||
|
||||
|
@ -47,13 +52,14 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(isInitialSync:
|
|||
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
|
||||
|
||||
rootThreadEvent?.markEventAsRoot(
|
||||
isInitialSync = isInitialSync,
|
||||
currentUserId = currentUserId,
|
||||
threadsCounted = it.size,
|
||||
latestMessageTimelineEventEntity = latestMessage
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
updateNotificationsNew(roomId, realm, currentUserId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -71,19 +77,11 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? =
|
|||
* Mark or update the current event a root thread event
|
||||
*/
|
||||
internal fun EventEntity.markEventAsRoot(
|
||||
isInitialSync: Boolean,
|
||||
currentUserId: String?,
|
||||
threadsCounted: Int,
|
||||
latestMessageTimelineEventEntity: TimelineEventEntity?) {
|
||||
isRootThread = true
|
||||
numberOfThreads = threadsCounted
|
||||
threadSummaryLatestMessage = latestMessageTimelineEventEntity
|
||||
// skip notification coming from messages from the same user, also retain already marked events
|
||||
hasUnreadThreadMessages = if (hasUnreadThreadMessages) {
|
||||
latestMessageTimelineEventEntity?.root?.sender != currentUserId
|
||||
} else {
|
||||
if (latestMessageTimelineEventEntity?.root?.sender == currentUserId) false else !isInitialSync
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,7 +113,9 @@ internal fun TimelineEventEntity.Companion.findAllLocalThreadNotificationsForRoo
|
|||
TimelineEventEntity
|
||||
.whereRoomId(realm, roomId = roomId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.HAS_UNREAD_THREAD_MESSAGES, true)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_MESSAGE.name)
|
||||
.or()
|
||||
.equalTo(TimelineEventEntityFields.ROOT.THREAD_NOTIFICATION_STATE_STR, ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE.name)
|
||||
|
||||
/**
|
||||
* Returns whether or not the given user is participating in a current thread
|
||||
|
@ -131,3 +131,115 @@ internal fun TimelineEventEntity.Companion.isUserParticipatingInThread(realm: Re
|
|||
.findFirst()
|
||||
?.let { true }
|
||||
?: false
|
||||
|
||||
/**
|
||||
* Returns whether or not the given user is mentioned in a current thread
|
||||
* @param roomId the room that the thread exists
|
||||
* @param rootThreadEventId the thread that the search will be done
|
||||
* @param userId the user that will try to find if there is a mention
|
||||
*/
|
||||
internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm, roomId: String, rootThreadEventId: String, userId: String): Boolean =
|
||||
TimelineEventEntity
|
||||
.whereRoomId(realm, roomId = roomId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.SENDER, userId)
|
||||
.findAll()
|
||||
.firstOrNull { isUserMentioned(userId, it) }
|
||||
?.let { true }
|
||||
?: false
|
||||
|
||||
/**
|
||||
* Find the read receipt for the current user
|
||||
*/
|
||||
internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
|
||||
ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
|
||||
.findFirst()
|
||||
?.eventId
|
||||
|
||||
/**
|
||||
* Returns whether or not the user is mentioned in the event
|
||||
*/
|
||||
internal fun isUserMentioned(currentUserId: String, timelineEventEntity: TimelineEventEntity?): Boolean {
|
||||
val decryptedContent = timelineEventEntity?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
|
||||
return decryptedContent.contains(currentUserId.replace("@", "").substringBefore(":"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update badge notifications. Count the number of new thread events after the latest
|
||||
* read receipt and aggregate. This function will find and notify new thread events
|
||||
* that the user is either mentioned, or the user had participated in.
|
||||
* Important: If the root thread event is not fetched notification will not work
|
||||
* Important: It will work only with the latest chunk, while read marker will be changed
|
||||
* immediately so we should not display wrong notifications
|
||||
*/
|
||||
internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
|
||||
|
||||
val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
|
||||
|
||||
val readReceiptChunk = ChunkEntity
|
||||
.findIncludingEvent(realm, readReceipt) ?: return
|
||||
|
||||
val readReceiptChunkTimelineEvents = readReceiptChunk
|
||||
.timelineEvents
|
||||
.where()
|
||||
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||
.findAll() ?: return
|
||||
|
||||
val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
|
||||
|
||||
if (readReceiptChunkPosition != -1 && readReceiptChunkPosition != readReceiptChunkTimelineEvents.lastIndex) {
|
||||
// If the read receipt is found inside the chunk
|
||||
|
||||
val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
|
||||
.slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
|
||||
.filter { it.root?.isThread() == true }
|
||||
|
||||
// In order for the below code to work for old events, we should save the previous read receipt
|
||||
// and then continue with the chunk search for that read receipt
|
||||
/*
|
||||
val newThreadEventsList = arrayListOf<TimelineEventEntity>()
|
||||
newThreadEventsList.addAll(threadEventsAfterReadReceipt)
|
||||
|
||||
// got from latest chunk all new threads, lets move to the others
|
||||
var nextChunk = ChunkEntity
|
||||
.find(realm = realm, roomId = roomId, nextToken = readReceiptChunk.nextToken)
|
||||
.takeIf { readReceiptChunk.nextToken != null }
|
||||
while (nextChunk != null) {
|
||||
newThreadEventsList.addAll(nextChunk.timelineEvents
|
||||
.filter { it.root?.isThread() == true })
|
||||
nextChunk = ChunkEntity
|
||||
.find(realm = realm, roomId = roomId, nextToken = nextChunk.nextToken)
|
||||
.takeIf { readReceiptChunk.nextToken != null }
|
||||
}*/
|
||||
|
||||
// Find if the user is mentioned in those events
|
||||
val userMentionsList = threadEventsAfterReadReceipt
|
||||
.filter {
|
||||
isUserMentioned(currentUserId = currentUserId, it)
|
||||
}.map {
|
||||
it.root?.rootThreadEventId
|
||||
}
|
||||
|
||||
// Find the root events in the new thread events
|
||||
val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
|
||||
|
||||
// Update root thread events only if the user have participated in
|
||||
rootThreads.forEach { eventId ->
|
||||
val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
rootThreadEventId = eventId,
|
||||
senderId = currentUserId)
|
||||
val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
|
||||
|
||||
if (isUserParticipating) {
|
||||
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
|
||||
}
|
||||
|
||||
if (userMentionsList.contains(eventId)) {
|
||||
rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
|
|||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
@ -55,9 +56,9 @@ internal object EventMapper {
|
|||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
||||
eventEntity.hasUnreadThreadMessages = event.threadDetails?.hasUnreadMessage ?: false
|
||||
eventEntity.rootThreadEventId = event.getRootThreadEventId()
|
||||
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
|
||||
eventEntity.threadNotificationState = event.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE
|
||||
return eventEntity
|
||||
}
|
||||
|
||||
|
@ -100,7 +101,6 @@ internal object EventMapper {
|
|||
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||
}
|
||||
it.mCryptoErrorReason = eventEntity.decryptionErrorReason
|
||||
|
||||
it.threadDetails = ThreadDetails(
|
||||
isRootThread = eventEntity.isRootThread,
|
||||
numberOfThreads = eventEntity.numberOfThreads,
|
||||
|
@ -112,7 +112,7 @@ internal object EventMapper {
|
|||
avatarUrl = timelineEventEntity.senderAvatar
|
||||
)
|
||||
},
|
||||
hasUnreadMessage = eventEntity.hasUnreadThreadMessages,
|
||||
threadNotificationState = eventEntity.threadNotificationState,
|
||||
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary().orEmpty()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model
|
|||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Index
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
@ -46,7 +47,7 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
@Index var isRootThread: Boolean = false,
|
||||
@Index var rootThreadEventId: String? = null,
|
||||
var numberOfThreads: Int = 0,
|
||||
var hasUnreadThreadMessages: Boolean = false,
|
||||
// var threadNotificationState: Boolean = false,
|
||||
var threadSummaryLatestMessage: TimelineEventEntity? = null
|
||||
|
||||
) : RealmObject() {
|
||||
|
@ -61,6 +62,15 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
sendStateStr = value.name
|
||||
}
|
||||
|
||||
private var threadNotificationStateStr: String = ThreadNotificationState.NO_NEW_MESSAGE.name
|
||||
var threadNotificationState: ThreadNotificationState
|
||||
get() {
|
||||
return ThreadNotificationState.valueOf(threadNotificationStateStr)
|
||||
}
|
||||
set(value) {
|
||||
threadNotificationStateStr = value.name
|
||||
}
|
||||
|
||||
companion object
|
||||
|
||||
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
||||
|
|
|
@ -169,7 +169,7 @@ internal class DefaultTimeline(
|
|||
.whereRoomId(realm, roomId = roomId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
|
||||
.or()
|
||||
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, it)
|
||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.findAll()
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
|||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.database.RealmSessionProvider
|
||||
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
|
||||
|
@ -41,6 +42,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
|||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
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.session.room.membership.LoadRoomMembersTask
|
||||
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
|
@ -48,6 +50,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction
|
|||
|
||||
internal class DefaultTimelineService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
@UserId private val userId: String,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val realmSessionProvider: RealmSessionProvider,
|
||||
private val timelineInput: TimelineInput,
|
||||
|
@ -137,13 +140,13 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun isUserParticipatingInThread(rootThreadEventId: String, senderId: String): Boolean {
|
||||
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
|
||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||
TimelineEventEntity.isUserParticipatingInThread(
|
||||
realm = it,
|
||||
roomId = roomId,
|
||||
rootThreadEventId = rootThreadEventId,
|
||||
senderId = senderId)
|
||||
senderId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,7 +154,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
monarchy.awaitTransaction {
|
||||
EventEntity.where(
|
||||
realm = it,
|
||||
eventId = rootThreadEventId).findFirst()?.hasUnreadThreadMessages = false
|
||||
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
|||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import org.matrix.android.sdk.BuildConfig
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.isThread
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
@ -44,8 +41,8 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
|
|||
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.where
|
||||
import org.matrix.android.sdk.internal.database.query.whereRootThreadEventId
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import timber.log.Timber
|
||||
|
@ -54,7 +51,9 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||
*/
|
||||
internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
|
||||
internal class TokenChunkEventPersistor @Inject constructor(
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
@UserId private val userId: String) {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
|
@ -213,7 +212,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
}
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
|
||||
val optimizedThreadSummaryMap = hashMapOf<String,EventEntity>()
|
||||
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||
eventList.forEach { event ->
|
||||
if (event.eventId == null || event.senderId == null) {
|
||||
return@forEach
|
||||
|
@ -260,16 +259,12 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null ||
|
||||
(chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
|
||||
if (shouldUpdateSummary) {
|
||||
// TODO maybe add support to view latest thread message
|
||||
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||
}
|
||||
if (currentChunk.isValid) {
|
||||
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
|
||||
}
|
||||
|
||||
// passing isInitialSync = true because we want to disable local notifications
|
||||
// they do not work properly without the API
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(true)
|
||||
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(roomId = roomId, realm = realm, currentUserId = userId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -425,7 +425,10 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||
}
|
||||
}
|
||||
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(insertType == EventInsertType.INITIAL_SYNC, userId)
|
||||
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
|
||||
roomId = roomId,
|
||||
realm = realm,
|
||||
currentUserId = userId)
|
||||
|
||||
// posting new events to timeline if any is registered
|
||||
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
||||
|
|
|
@ -94,6 +94,8 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
|||
import org.matrix.android.sdk.api.session.room.read.ReadService
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
|
@ -291,7 +293,14 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
room.flow()
|
||||
.liveLocalUnreadThreadList()
|
||||
.execute {
|
||||
copy(numberOfLocalUnreadThreads = it.invoke()?.size ?: 0)
|
||||
val threadList = it.invoke()
|
||||
val isUserMentioned = threadList?.firstOrNull { timelineEvent ->
|
||||
timelineEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
|
||||
}?.let { true } ?: false
|
||||
val numberOfLocalUnreadThreads = threadList?.size ?: 0
|
||||
copy(threadNotificationBadgeState = ThreadNotificationBadgeState(
|
||||
numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
|
||||
isUserMentioned = isUserMentioned))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1178,6 +1187,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
chatEffectManager.delegate = null
|
||||
chatEffectManager.dispose()
|
||||
callManager.removeProtocolsCheckerListener(this)
|
||||
markThreadTimelineAsReadLocal()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
|||
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.sync.SyncState
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
|
||||
import org.matrix.android.sdk.api.session.widgets.model.Widget
|
||||
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
|
||||
|
||||
|
@ -68,7 +69,7 @@ data class RoomDetailViewState(
|
|||
val hasFailedSending: Boolean = false,
|
||||
val jitsiState: JitsiState = JitsiState(),
|
||||
val rootThreadEventId: String? = null,
|
||||
val numberOfLocalUnreadThreads: Int = 0
|
||||
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState()
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: TimelineArgs) : this(
|
||||
|
|
|
@ -1031,8 +1031,8 @@ class TimelineFragment @Inject constructor(
|
|||
val badgeFrameLayout = menuThreadList.findViewById<FrameLayout>(R.id.threadNotificationBadgeFrameLayout)
|
||||
val badgeTextView = menuThreadList.findViewById<TextView>(R.id.threadNotificationBadgeTextView)
|
||||
|
||||
val unreadThreadMessages = state.numberOfLocalUnreadThreads
|
||||
val userIsMentioned = false
|
||||
val unreadThreadMessages = state.threadNotificationBadgeState.numberOfLocalUnreadThreads
|
||||
val userIsMentioned = state.threadNotificationBadgeState.isUserMentioned
|
||||
|
||||
if (unreadThreadMessages > 0) {
|
||||
badgeFrameLayout.isVisible = true
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.threads.list.model
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
|
@ -32,6 +33,7 @@ import im.vector.app.core.extensions.setLeftDrawable
|
|||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_thread_list)
|
||||
|
@ -43,7 +45,7 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
|||
@EpoxyAttribute lateinit var date: String
|
||||
@EpoxyAttribute lateinit var rootMessage: String
|
||||
@EpoxyAttribute lateinit var lastMessage: String
|
||||
@EpoxyAttribute var unreadMessage: Boolean = false
|
||||
@EpoxyAttribute var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
|
||||
@EpoxyAttribute lateinit var lastMessageCounter: String
|
||||
@EpoxyAttribute var rootMessageDeleted: Boolean = false
|
||||
@EpoxyAttribute var lastMessageMatrixItem: MatrixItem? = null
|
||||
|
@ -56,11 +58,11 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
|||
holder.avatarImageView.contentDescription = matrixItem.getBestName()
|
||||
holder.titleTextView.text = title
|
||||
holder.dateTextView.text = date
|
||||
if (rootMessageDeleted){
|
||||
if (rootMessageDeleted) {
|
||||
holder.rootMessageTextView.text = holder.view.context.getString(R.string.event_redacted)
|
||||
holder.rootMessageTextView.setLeftDrawable(R.drawable.ic_trash_16, R.attr.colorOnPrimary)
|
||||
holder.rootMessageTextView.compoundDrawablePadding = DimensionConverter(holder.view.context.resources).dpToPx(10)
|
||||
}else{
|
||||
} else {
|
||||
holder.rootMessageTextView.text = rootMessage
|
||||
holder.rootMessageTextView.clearDrawables()
|
||||
}
|
||||
|
@ -71,7 +73,24 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
|||
holder.lastMessageAvatarImageView.contentDescription = lastMessageMatrixItem?.getBestName()
|
||||
holder.lastMessageTextView.text = lastMessage
|
||||
holder.lastMessageCounterTextView.text = lastMessageCounter
|
||||
holder.unreadImageView.isVisible = unreadMessage
|
||||
renderNotificationState(holder)
|
||||
}
|
||||
|
||||
private fun renderNotificationState(holder: Holder) {
|
||||
|
||||
when (threadNotificationState) {
|
||||
ThreadNotificationState.NEW_MESSAGE -> {
|
||||
holder.unreadImageView.isVisible = true
|
||||
holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_gray_200));
|
||||
}
|
||||
ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE -> {
|
||||
holder.unreadImageView.isVisible = true
|
||||
holder.unreadImageView.setColorFilter(ContextCompat.getColor(holder.view.context, R.color.palette_vermilion));
|
||||
}
|
||||
else -> {
|
||||
holder.unreadImageView.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
|
@ -83,7 +102,6 @@ abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
|
|||
val lastMessageCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
|
||||
val lastMessageTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
|
||||
val unreadImageView by bind<ImageView>(R.id.threadSummaryUnreadImageView)
|
||||
|
||||
val rootView by bind<ConstraintLayout>(R.id.threadSummaryRootConstraintLayout)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import im.vector.app.core.date.VectorDateFormatter
|
|||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.threads.list.model.threadList
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -44,6 +45,15 @@ class ThreadListController @Inject constructor(
|
|||
val host = this
|
||||
|
||||
safeViewState.rootThreadEventList.invoke()
|
||||
?.filter {
|
||||
if (safeViewState.shouldFilterThreads) {
|
||||
it.isParticipating
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}?.map {
|
||||
it.timelineEvent
|
||||
}
|
||||
?.forEach { timelineEvent ->
|
||||
val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
|
||||
threadList {
|
||||
|
@ -53,7 +63,7 @@ class ThreadListController @Inject constructor(
|
|||
title(timelineEvent.senderInfo.displayName)
|
||||
date(date)
|
||||
rootMessageDeleted(timelineEvent.root.isRedacted())
|
||||
unreadMessage(timelineEvent.root.threadDetails?.hasUnreadMessage ?: false)
|
||||
threadNotificationState(timelineEvent.root.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE)
|
||||
rootMessage(timelineEvent.root.getDecryptedTextSummary())
|
||||
lastMessage(timelineEvent.root.threadDetails?.threadSummaryLatestTextMessage.orEmpty())
|
||||
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
|
||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
|
|||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
|
||||
import org.matrix.android.sdk.flow.flow
|
||||
|
||||
class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState,
|
||||
|
@ -52,28 +53,29 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
|
|||
}
|
||||
|
||||
init {
|
||||
observeThreadsList(initialState.shouldFilterThreads)
|
||||
observeThreadsList()
|
||||
}
|
||||
|
||||
override fun handle(action: EmptyAction) {}
|
||||
|
||||
private fun observeThreadsList(shouldFilterThreads: Boolean) =
|
||||
room?.flow()
|
||||
?.liveThreadList()
|
||||
?.map {
|
||||
if (!shouldFilterThreads) return@map it
|
||||
it.filter { timelineEvent ->
|
||||
room.isUserParticipatingInThread(timelineEvent.eventId, session.myUserId)
|
||||
}
|
||||
}
|
||||
?.flowOn(room.coroutineDispatchers.io)
|
||||
?.execute { asyncThreads ->
|
||||
copy(
|
||||
rootThreadEventList = asyncThreads,
|
||||
shouldFilterThreads = shouldFilterThreads)
|
||||
private fun observeThreadsList() {
|
||||
room?.flow()
|
||||
?.liveThreadList()
|
||||
?.map {
|
||||
it.map { timelineEvent ->
|
||||
val isParticipating = room.isUserParticipatingInThread(timelineEvent.eventId)
|
||||
ThreadTimelineEvent(timelineEvent, isParticipating)
|
||||
}
|
||||
}
|
||||
?.flowOn(room.coroutineDispatchers.io)
|
||||
?.execute { asyncThreads ->
|
||||
copy(rootThreadEventList = asyncThreads)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyFiltering(shouldFilterThreads: Boolean) {
|
||||
observeThreadsList(shouldFilterThreads)
|
||||
setState {
|
||||
copy(shouldFilterThreads = shouldFilterThreads)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ import com.airbnb.mvrx.Async
|
|||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
|
||||
|
||||
data class ThreadListViewState(
|
||||
val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized,
|
||||
val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized,
|
||||
val shouldFilterThreads: Boolean = false,
|
||||
val roomId: String
|
||||
) : MavericksState{
|
||||
|
|
Loading…
Add table
Reference in a new issue