Add total unread notification count to home screen (#7891)

* Add total unread notification count to home screen

This will be calculated on each sync heartbeat (for now, using the state updates for the room list service).

* Simplify `counter` parameter logic

* Read the total unread count when the home screen is first displayed

---------

Co-authored-by: Benoit Marty <benoitm@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-10-13 12:58:07 +02:00
committed by GitHub
parent 50aa723f63
commit 9cc5cf9a01
13 changed files with 121 additions and 14 deletions
@@ -505,6 +505,12 @@ interface MatrixClient : ClientUrlContentFetcher {
/** Returns a provider for the capabilities the homeserver advertises, such as whether the display name can be changed. */
fun homeserverCapabilities(): HomeserverCapabilitiesProvider
/**
* Returns the total number of unread notifications across all rooms, as currently known by the client.
* This is a snapshot and may be out of date if the sync is running.
*/
fun totalUnreadCount(): Result<Long>
}
/**
@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.api.sync
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
/**
@@ -37,4 +38,10 @@ interface SyncService {
* This is `false` only while the state is [SyncState.Offline]; an idle or errored sync still counts as online.
*/
val isOnline: StateFlow<Boolean>
/**
* Flow that will emit every time the sync has received a response from the server, even if it was empty.
* Useful for triggering UI updates when the sync is running.
*/
val heartBeat: Flow<Unit>
}
@@ -1024,6 +1024,10 @@ class RustMatrixClient(
return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities())
}
override fun totalUnreadCount(): Result<Long> = runCatchingExceptions {
innerClient.totalUnreadNotifications().toLong()
}
private fun scheduleBackgroundSearchBackfill() {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MessageSearch)
.distinctUntilChanged()
@@ -12,16 +12,20 @@ import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.impl.roomlist.stateFlow
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.SyncServiceState
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
@@ -78,4 +82,10 @@ class RustSyncService(
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
// TODO actually use a heartbeat mechanism from the SDK, not simulate it with the room list service
override val heartBeat: Flow<Unit> = inner.roomListService()
.stateFlow()
.filter { it == RoomListServiceState.RUNNING }
.map {}
}
@@ -102,6 +102,7 @@ class FakeMatrixClient(
override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
private val totalUnreadCountResult: () -> Result<Long> = { Result.success(0L) },
private val accountManagementUrlResult: (AccountManagementAction?) -> Result<String?> = { lambdaError() },
private val resolveRoomAliasResult: suspend (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
Result.success(
@@ -468,4 +469,8 @@ class FakeMatrixClient(
override fun homeserverCapabilities(): HomeserverCapabilitiesProvider {
return homeserverCapabilitiesProvider
}
override fun totalUnreadCount(): Result<Long> {
return totalUnreadCountResult()
}
}
@@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.test.sync
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -33,6 +35,14 @@ class FakeSyncService(
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
private val heartBeatFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
override val heartBeat: Flow<Unit> = heartBeatFlow
suspend fun emitHeartBeat() {
heartBeatFlow.emit(Unit)
}
suspend fun emitSyncState(syncState: SyncState) {
syncStateFlow.emit(syncState)
}