mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 08:15:02 +00:00
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:
committed by
GitHub
parent
50aa723f63
commit
9cc5cf9a01
@@ -18,7 +18,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
|
|||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
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.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@@ -34,7 +34,7 @@ class LoggedInEventProcessor(
|
|||||||
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
|
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
private val roomMembershipObserver: RoomMembershipObserver,
|
private val roomMembershipObserver: RoomMembershipObserver,
|
||||||
private val sessionId: SessionId,
|
private val sessionId: SessionId,
|
||||||
private val roomListService: RoomListService,
|
private val syncService: SyncService,
|
||||||
private val notificationCleaner: NotificationCleaner,
|
private val notificationCleaner: NotificationCleaner,
|
||||||
private val dispatchers: CoroutineDispatchers
|
private val dispatchers: CoroutineDispatchers
|
||||||
) {
|
) {
|
||||||
@@ -73,9 +73,8 @@ class LoggedInEventProcessor(
|
|||||||
}
|
}
|
||||||
.launchIn(childScope)
|
.launchIn(childScope)
|
||||||
|
|
||||||
// Use the room list service state as a 'heartbeat' update to check existing notifications
|
// Use the sync service heartbeat as a 'heartbeat' update to check existing notifications
|
||||||
roomListService.state
|
syncService.heartBeat
|
||||||
.filter { it == RoomListService.State.Running }
|
|
||||||
.onEach {
|
.onEach {
|
||||||
notificationCleaner.clearReadRoomsNotifications(sessionId)
|
notificationCleaner.clearReadRoomsNotifications(sessionId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch
|
|||||||
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.room.CurrentUserMembership
|
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
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
|
||||||
@@ -155,24 +154,24 @@ class LoggedInEventProcessorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test notifications are cleared when room list service state is running`() = runTest {
|
fun `test notifications are cleared when sync heartbeat is emitted`() = runTest {
|
||||||
val clearReadNotificationsLambda = lambdaRecorder { _: SessionId -> }
|
val clearReadNotificationsLambda = lambdaRecorder { _: SessionId -> }
|
||||||
val snackbarDispatcher = SnackbarDispatcher()
|
val snackbarDispatcher = SnackbarDispatcher()
|
||||||
val notificationCleaner = FakeNotificationCleaner(clearReadRoomsNotificationsLambda = clearReadNotificationsLambda)
|
val notificationCleaner = FakeNotificationCleaner(clearReadRoomsNotificationsLambda = clearReadNotificationsLambda)
|
||||||
val roomListService = FakeRoomListService()
|
val syncService = FakeSyncService()
|
||||||
|
|
||||||
val loggedInEventProcessor = createLoggedInEventProcessor(
|
val loggedInEventProcessor = createLoggedInEventProcessor(
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
notificationCleaner = notificationCleaner,
|
notificationCleaner = notificationCleaner,
|
||||||
roomListService = roomListService,
|
syncService = syncService,
|
||||||
)
|
)
|
||||||
|
|
||||||
loggedInEventProcessor.observeEvents(backgroundScope)
|
loggedInEventProcessor.observeEvents(backgroundScope)
|
||||||
|
|
||||||
testScheduler.runCurrent()
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
// Simulate the room list service state changing to Running
|
// Simulate a sync heartbeat
|
||||||
roomListService.postState(RoomListService.State.Running)
|
syncService.emitHeartBeat()
|
||||||
|
|
||||||
testScheduler.runCurrent()
|
testScheduler.runCurrent()
|
||||||
|
|
||||||
@@ -184,13 +183,13 @@ class LoggedInEventProcessorTest {
|
|||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||||
sessionId: SessionId = A_SESSION_ID,
|
sessionId: SessionId = A_SESSION_ID,
|
||||||
roomListService: FakeRoomListService = FakeRoomListService(),
|
syncService: FakeSyncService = FakeSyncService(),
|
||||||
notificationCleaner: FakeNotificationCleaner = FakeNotificationCleaner(),
|
notificationCleaner: FakeNotificationCleaner = FakeNotificationCleaner(),
|
||||||
) = LoggedInEventProcessor(
|
) = LoggedInEventProcessor(
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
roomMembershipObserver = roomMembershipObserver,
|
roomMembershipObserver = roomMembershipObserver,
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
roomListService = roomListService,
|
syncService = syncService,
|
||||||
notificationCleaner = notificationCleaner,
|
notificationCleaner = notificationCleaner,
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
)
|
)
|
||||||
|
|||||||
+17
@@ -14,6 +14,7 @@ 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
|
||||||
@@ -34,9 +35,14 @@ 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.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(
|
||||||
@@ -53,10 +59,20 @@ 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 produceState(client.totalUnreadCount().getOrDefault(0).toInt()) {
|
||||||
|
syncService.heartBeat
|
||||||
|
// 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,
|
||||||
@@ -130,6 +146,7 @@ class HomePresenter(
|
|||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
canReportBug = canReportBug,
|
canReportBug = canReportBug,
|
||||||
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
showMultiAccountAnnouncement = showMultiAccountAnnouncement,
|
||||||
|
totalUnreadCount = totalUnreadCount,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ data class HomeState(
|
|||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val canReportBug: Boolean,
|
val canReportBug: Boolean,
|
||||||
val showMultiAccountAnnouncement: Boolean,
|
val showMultiAccountAnnouncement: Boolean,
|
||||||
|
val totalUnreadCount: Int,
|
||||||
val eventSink: (HomeEvent) -> Unit,
|
val eventSink: (HomeEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
|
||||||
|
|||||||
+2
@@ -64,6 +64,7 @@ internal fun aHomeState(
|
|||||||
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
homeSpacesState: HomeSpacesState = aHomeSpacesState(),
|
||||||
canReportBug: Boolean = true,
|
canReportBug: Boolean = true,
|
||||||
showMultiAccountAnnouncement: Boolean = false,
|
showMultiAccountAnnouncement: Boolean = false,
|
||||||
|
totalUnreadCount: Int = 0,
|
||||||
eventSink: (HomeEvent) -> Unit = {}
|
eventSink: (HomeEvent) -> Unit = {}
|
||||||
) = HomeState(
|
) = HomeState(
|
||||||
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
currentUserAndNeighbors = currentUserAndNeighbors.toImmutableList(),
|
||||||
@@ -75,5 +76,6 @@ internal fun aHomeState(
|
|||||||
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
currentHomeNavigationBarItem = currentHomeNavigationBarItem,
|
||||||
roomListState = roomListState,
|
roomListState = roomListState,
|
||||||
homeSpacesState = homeSpacesState,
|
homeSpacesState = homeSpacesState,
|
||||||
|
totalUnreadCount = totalUnreadCount,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ private fun HomeScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
totalUnreadCount = state.totalUnreadCount,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.Center,
|
floatingActionButtonPosition = FabPosition.Center,
|
||||||
@@ -341,6 +342,7 @@ private fun HomeFloatingActionButton(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HomeBottomBar(
|
private fun HomeBottomBar(
|
||||||
|
totalUnreadCount: Int,
|
||||||
currentHomeNavigationBarItem: HomeNavigationBarItem,
|
currentHomeNavigationBarItem: HomeNavigationBarItem,
|
||||||
onItemClick: (HomeNavigationBarItem) -> Unit,
|
onItemClick: (HomeNavigationBarItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -361,6 +363,7 @@ private fun HomeBottomBar(
|
|||||||
tooltipLabel = stringResource(item.labelRes),
|
tooltipLabel = stringResource(item.labelRes),
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
onClick = { onItemClick(item) },
|
onClick = { onItemClick(item) },
|
||||||
|
counter = totalUnreadCount.takeIf { item == HomeNavigationBarItem.Chats && it > 0 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
@@ -40,9 +40,11 @@ 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.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class HomePresenterTest {
|
class HomePresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
@@ -117,6 +119,38 @@ class HomePresenterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - total unread count is updated on sync heartbeat`() = runTest {
|
||||||
|
var unreadCount = 0L
|
||||||
|
val syncService = FakeSyncService()
|
||||||
|
val presenter = createHomePresenter(
|
||||||
|
client = FakeMatrixClient(totalUnreadCountResult = { Result.success(unreadCount) }),
|
||||||
|
syncService = syncService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().totalUnreadCount).isEqualTo(0)
|
||||||
|
unreadCount = 5
|
||||||
|
syncService.emitHeartBeat()
|
||||||
|
advanceTimeBy(200.milliseconds)
|
||||||
|
assertThat(awaitItem().totalUnreadCount).isEqualTo(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - total unread count defaults to 0 when the SDK call fails`() = runTest {
|
||||||
|
val syncService = FakeSyncService()
|
||||||
|
val presenter = createHomePresenter(
|
||||||
|
client = FakeMatrixClient(totalUnreadCountResult = { Result.failure(AN_EXCEPTION) }),
|
||||||
|
syncService = syncService,
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().totalUnreadCount).isEqualTo(0)
|
||||||
|
syncService.emitHeartBeat()
|
||||||
|
advanceTimeBy(200.milliseconds)
|
||||||
|
expectNoEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with no user and then load user with error`() = runTest {
|
fun `present - should start with no user and then load user with error`() = runTest {
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
@@ -245,6 +279,16 @@ class HomePresenterTest {
|
|||||||
.with(value(Announcement.MultiAccount))
|
.with(value(Announcement.MultiAccount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - total unread count is loaded in the initial state`() = runTest {
|
||||||
|
val presenter = createHomePresenter(
|
||||||
|
client = FakeMatrixClient(totalUnreadCountResult = { Result.success(5) }),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().totalUnreadCount).isEqualTo(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun createHomePresenter(
|
internal fun createHomePresenter(
|
||||||
|
|||||||
+6
@@ -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. */
|
/** Returns a provider for the capabilities the homeserver advertises, such as whether the display name can be changed. */
|
||||||
fun homeserverCapabilities(): HomeserverCapabilitiesProvider
|
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>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+7
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.libraries.matrix.api.sync
|
package io.element.android.libraries.matrix.api.sync
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.
|
* This is `false` only while the state is [SyncState.Offline]; an idle or errored sync still counts as online.
|
||||||
*/
|
*/
|
||||||
val isOnline: StateFlow<Boolean>
|
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>
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -1024,6 +1024,10 @@ class RustMatrixClient(
|
|||||||
return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities())
|
return RustHomeserverCapabilitiesProvider(innerClient.homeserverCapabilities())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun totalUnreadCount(): Result<Long> = runCatchingExceptions {
|
||||||
|
innerClient.totalUnreadNotifications().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
private fun scheduleBackgroundSearchBackfill() {
|
private fun scheduleBackgroundSearchBackfill() {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MessageSearch)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MessageSearch)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|||||||
+10
@@ -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.core.extensions.runCatchingExceptions
|
||||||
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.sync.SyncState
|
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.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomListServiceState
|
||||||
import org.matrix.rustcomponents.sdk.SyncServiceState
|
import org.matrix.rustcomponents.sdk.SyncServiceState
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@@ -78,4 +82,10 @@ class RustSyncService(
|
|||||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
|
||||||
|
|
||||||
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
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 {}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -102,6 +102,7 @@ class FakeMatrixClient(
|
|||||||
override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
override val mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
||||||
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(),
|
||||||
private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
|
private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(),
|
||||||
|
private val totalUnreadCountResult: () -> Result<Long> = { Result.success(0L) },
|
||||||
private val accountManagementUrlResult: (AccountManagementAction?) -> Result<String?> = { lambdaError() },
|
private val accountManagementUrlResult: (AccountManagementAction?) -> Result<String?> = { lambdaError() },
|
||||||
private val resolveRoomAliasResult: suspend (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
|
private val resolveRoomAliasResult: suspend (RoomAlias) -> Result<Optional<ResolvedRoomAlias>> = {
|
||||||
Result.success(
|
Result.success(
|
||||||
@@ -468,4 +469,8 @@ class FakeMatrixClient(
|
|||||||
override fun homeserverCapabilities(): HomeserverCapabilitiesProvider {
|
override fun homeserverCapabilities(): HomeserverCapabilitiesProvider {
|
||||||
return homeserverCapabilitiesProvider
|
return homeserverCapabilitiesProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun totalUnreadCount(): Result<Long> {
|
||||||
|
return totalUnreadCountResult()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -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.core.coroutine.mapState
|
||||||
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.sync.SyncState
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
@@ -33,6 +35,14 @@ class FakeSyncService(
|
|||||||
|
|
||||||
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
|
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) {
|
suspend fun emitSyncState(syncState: SyncState) {
|
||||||
syncStateFlow.emit(syncState)
|
syncStateFlow.emit(syncState)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user