mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Add MatrixClient.totalUnreadCountFlow() extension
Extract the logic to observe the total unread count of a MatrixClient, refreshed on each sync heartbeat, to a shared extension and use it in HomePresenter and PreferencesRootPresenter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-16
@@ -14,7 +14,6 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@@ -33,16 +32,12 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||||||
import io.element.android.libraries.indicator.api.IndicatorService
|
import io.element.android.libraries.indicator.api.IndicatorService
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
import io.element.android.libraries.matrix.api.totalUnreadCountFlow
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
class HomePresenter(
|
class HomePresenter(
|
||||||
@@ -59,20 +54,13 @@ class HomePresenter(
|
|||||||
) : Presenter<HomeState> {
|
) : Presenter<HomeState> {
|
||||||
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
private val currentUserWithNeighborsBuilder = CurrentUserWithNeighborsBuilder()
|
||||||
|
|
||||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): HomeState {
|
override fun present(): HomeState {
|
||||||
val coroutineState = rememberCoroutineScope()
|
val coroutineState = rememberCoroutineScope()
|
||||||
val matrixUser by client.userProfile.collectAsState()
|
val matrixUser by client.userProfile.collectAsState()
|
||||||
|
val totalUnreadCount by remember { client.totalUnreadCountFlow() }
|
||||||
val totalUnreadCount by produceState(client.totalUnreadCount().getOrDefault(0).toInt()) {
|
// The initial value is only used for the first composition, remember it to avoid calling the SDK on every recomposition.
|
||||||
syncService.heartBeat
|
.collectAsState(initial = remember { client.totalUnreadCount().getOrDefault(0).toInt() })
|
||||||
// Delay the heartbeat so the SDK has time to update the unread count before we read it.
|
|
||||||
.debounce(100.milliseconds)
|
|
||||||
.mapLatest { client.totalUnreadCount().getOrDefault(0).toInt() }
|
|
||||||
.collect { value = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentUserAndNeighbors by remember {
|
val currentUserAndNeighbors by remember {
|
||||||
combine(
|
combine(
|
||||||
client.userProfile,
|
client.userProfile,
|
||||||
|
|||||||
+11
-2
@@ -6,6 +6,8 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.features.home.impl
|
package io.element.android.features.home.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
@@ -39,6 +41,7 @@ import io.element.android.tests.testutils.WarmUpRule
|
|||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.advanceTimeBy
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -124,7 +127,10 @@ class HomePresenterTest {
|
|||||||
var unreadCount = 0L
|
var unreadCount = 0L
|
||||||
val syncService = FakeSyncService()
|
val syncService = FakeSyncService()
|
||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
client = FakeMatrixClient(totalUnreadCountResult = { Result.success(unreadCount) }),
|
client = FakeMatrixClient(
|
||||||
|
syncService = syncService,
|
||||||
|
totalUnreadCountResult = { Result.success(unreadCount) },
|
||||||
|
),
|
||||||
syncService = syncService,
|
syncService = syncService,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -140,7 +146,10 @@ class HomePresenterTest {
|
|||||||
fun `present - total unread count defaults to 0 when the SDK call fails`() = runTest {
|
fun `present - total unread count defaults to 0 when the SDK call fails`() = runTest {
|
||||||
val syncService = FakeSyncService()
|
val syncService = FakeSyncService()
|
||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
client = FakeMatrixClient(totalUnreadCountResult = { Result.failure(AN_EXCEPTION) }),
|
client = FakeMatrixClient(
|
||||||
|
syncService = syncService,
|
||||||
|
totalUnreadCountResult = { Result.failure(AN_EXCEPTION) },
|
||||||
|
),
|
||||||
syncService = syncService,
|
syncService = syncService,
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|||||||
+2
-12
@@ -32,6 +32,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
|
|||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.totalUnreadCountFlow
|
||||||
import io.element.android.libraries.matrix.api.user.toMatrixUser
|
import io.element.android.libraries.matrix.api.user.toMatrixUser
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
@@ -40,17 +41,12 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Inject
|
@Inject
|
||||||
@@ -182,14 +178,8 @@ class PreferencesRootPresenter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
|
||||||
private fun unreadCountFlow(sessionId: SessionId): Flow<Int> {
|
private fun unreadCountFlow(sessionId: SessionId): Flow<Int> {
|
||||||
val client = matrixClientProvider.getOrNull(sessionId) ?: return flowOf(0)
|
val client = matrixClientProvider.getOrNull(sessionId) ?: return flowOf(0)
|
||||||
return client.syncService.heartBeat
|
return client.totalUnreadCountFlow()
|
||||||
// Delay the heartbeat so the SDK has time to update the unread count before we read it.
|
|
||||||
.debounce(100.milliseconds)
|
|
||||||
.onStart { emit(Unit) }
|
|
||||||
.map { client.totalUnreadCount().getOrDefault(0).toInt() }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -51,9 +51,15 @@ import io.element.android.libraries.matrix.api.user.UserStatus
|
|||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry point for everything scoped to a single logged in session: the sub-services, room access, and account level operations.
|
* Entry point for everything scoped to a single logged in session: the sub-services, room access, and account level operations.
|
||||||
@@ -523,3 +529,18 @@ fun MatrixClient.roomAliasFromName(name: String): RoomAlias? {
|
|||||||
?.takeIf { MatrixPatterns.isRoomAlias(it) }
|
?.takeIf { MatrixPatterns.isRoomAlias(it) }
|
||||||
?.let { tryOrNull { RoomAlias(it) } }
|
?.let { tryOrNull { RoomAlias(it) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a flow of the total number of unread notifications across all rooms.
|
||||||
|
* The first value is emitted immediately, then the value is refreshed on each sync heartbeat.
|
||||||
|
* In case of error, 0 is emitted.
|
||||||
|
*/
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
fun MatrixClient.totalUnreadCountFlow(): Flow<Int> {
|
||||||
|
return syncService.heartBeat
|
||||||
|
// Delay the heartbeat so the SDK has time to update the unread count before we read it.
|
||||||
|
.debounce(100.milliseconds)
|
||||||
|
.onStart { emit(Unit) }
|
||||||
|
.map { totalUnreadCount().getOrDefault(0).toInt() }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user