From 512829dd4935742e959037b2d9fdc42b870dc842 Mon Sep 17 00:00:00 2001 From: Jens Reinemann Date: Mon, 18 May 2026 18:28:49 +0200 Subject: [PATCH] feat(messaging): ungelesene Nachrichten als Badges anzeigen (#110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB-Migration 7→8: is_read-Spalte in messages (default 1 für bestehende Rows) - DAO: getUnreadCountsBySender, getTotalUnreadCount, markConversationAsRead - Repository: totalUnreadCount Flow + getUnreadCountsBySender() + markConversationAsRead() - ChatViewModel: markConversationAsRead beim Öffnen/Empfangen - UserListViewModel: unreadCounts StateFlow - UserListScreen: rote Badge-Anzeige pro Chat - MainViewModel: totalUnreadCount StateFlow - MainScreen: BadgedBox am Nachrichten-Icon in Bottom Nav - NotificationHelper: updateBadgeCount() für Launcher-Badge - Tests: 3 neue Fälle, FakeDao+FakeRepo aktualisiert (328 Tests grün) --- .../bollwerk/app/data/db/BollwerkDatabase.kt | 5 +- .../de/bollwerk/app/data/db/dao/MessageDao.kt | 15 ++++++ .../app/data/db/entity/MessageEntity.kt | 5 +- .../data/repository/MessageRepositoryImpl.kt | 41 +++++++++++++-- .../domain/repository/MessageRepository.kt | 3 ++ .../app/notification/NotificationHelper.kt | 36 +++++++++++++ .../java/de/bollwerk/app/ui/MainScreen.kt | 52 ++++++++++++++----- .../java/de/bollwerk/app/ui/MainViewModel.kt | 10 +++- .../app/ui/messaging/ChatViewModel.kt | 1 + .../app/ui/messaging/UserListScreen.kt | 16 +++++- .../app/ui/messaging/UserListViewModel.kt | 5 ++ .../repository/MessageRepositoryImplTest.kt | 33 ++++++++++++ .../app/ui/messaging/ChatViewModelTest.kt | 4 ++ .../app/ui/messaging/UserListViewModelTest.kt | 4 ++ 14 files changed, 208 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/de/bollwerk/app/data/db/BollwerkDatabase.kt b/app/src/main/java/de/bollwerk/app/data/db/BollwerkDatabase.kt index 7bc10d7..ad27ef5 100644 --- a/app/src/main/java/de/bollwerk/app/data/db/BollwerkDatabase.kt +++ b/app/src/main/java/de/bollwerk/app/data/db/BollwerkDatabase.kt @@ -19,11 +19,12 @@ import de.bollwerk.app.data.db.entity.SettingsEntity @Database( entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class, SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class], - version = 7, + version = 8, exportSchema = true, autoMigrations = [ AutoMigration(from = 5, to = 6), - AutoMigration(from = 6, to = 7) + AutoMigration(from = 6, to = 7), + AutoMigration(from = 7, to = 8) ] ) @TypeConverters(LocalDateConverter::class) diff --git a/app/src/main/java/de/bollwerk/app/data/db/dao/MessageDao.kt b/app/src/main/java/de/bollwerk/app/data/db/dao/MessageDao.kt index 093277b..c826ebd 100644 --- a/app/src/main/java/de/bollwerk/app/data/db/dao/MessageDao.kt +++ b/app/src/main/java/de/bollwerk/app/data/db/dao/MessageDao.kt @@ -1,5 +1,6 @@ package de.bollwerk.app.data.db.dao +import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy @@ -8,6 +9,11 @@ import androidx.room.Upsert import de.bollwerk.app.data.db.entity.MessageEntity import kotlinx.coroutines.flow.Flow +internal data class UnreadCountBySender( + @ColumnInfo(name = "sender_id") val senderId: String, + @ColumnInfo(name = "count") val count: Int +) + @Dao internal interface MessageDao { @@ -30,4 +36,13 @@ internal interface MessageDao { @Query("UPDATE messages SET is_pending = 0 WHERE id = :id") suspend fun markDelivered(id: String) + + @Query("SELECT sender_id, COUNT(*) as count FROM messages WHERE receiver_id = :myId AND is_read = 0 GROUP BY sender_id") + fun getUnreadCountsBySender(myId: String): Flow> + + @Query("SELECT COUNT(*) FROM messages WHERE receiver_id = :myId AND is_read = 0") + fun getTotalUnreadCount(myId: String): Flow + + @Query("UPDATE messages SET is_read = 1 WHERE receiver_id = :myId AND sender_id = :senderId") + suspend fun markConversationAsRead(myId: String, senderId: String) } diff --git a/app/src/main/java/de/bollwerk/app/data/db/entity/MessageEntity.kt b/app/src/main/java/de/bollwerk/app/data/db/entity/MessageEntity.kt index 83fee9a..e7aa89e 100644 --- a/app/src/main/java/de/bollwerk/app/data/db/entity/MessageEntity.kt +++ b/app/src/main/java/de/bollwerk/app/data/db/entity/MessageEntity.kt @@ -12,5 +12,8 @@ internal data class MessageEntity( @ColumnInfo(name = "receiver_id") val receiverId: String, @ColumnInfo(name = "body") val body: String, @ColumnInfo(name = "sent_at") val sentAt: Long, - @ColumnInfo(name = "is_pending") val isPending: Boolean + @ColumnInfo(name = "is_pending") val isPending: Boolean, + // defaultValue "1" = existing rows after migration are treated as already read; + // Kotlin default false = new objects start as unread (caller sets explicitly) + @ColumnInfo(name = "is_read", defaultValue = "1") val isRead: Boolean = false ) diff --git a/app/src/main/java/de/bollwerk/app/data/repository/MessageRepositoryImpl.kt b/app/src/main/java/de/bollwerk/app/data/repository/MessageRepositoryImpl.kt index 2ff6ec5..7aebb8b 100644 --- a/app/src/main/java/de/bollwerk/app/data/repository/MessageRepositoryImpl.kt +++ b/app/src/main/java/de/bollwerk/app/data/repository/MessageRepositoryImpl.kt @@ -28,7 +28,11 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable @@ -50,6 +54,7 @@ private data class SendMessageRequest( @Serializable private data class PublicKeyResponse(val publicKey: String) +@OptIn(ExperimentalCoroutinesApi::class) @Singleton internal class MessageRepositoryImpl @Inject constructor( private val dao: MessageDao, @@ -84,7 +89,8 @@ internal class MessageRepositoryImpl @Inject constructor( receiverId = msg.receiverId, body = decryptedBody, sentAt = msg.sentAt, - isPending = false + isPending = false, + isRead = false ) ) notificationHelper.showNewMessageNotification( @@ -100,6 +106,9 @@ internal class MessageRepositoryImpl @Inject constructor( } } } + scope.launch { + totalUnreadCount.collect { count -> notificationHelper.updateBadgeCount(count) } + } } override fun getConversation(myId: String, otherId: String): Flow> = @@ -126,7 +135,8 @@ internal class MessageRepositoryImpl @Inject constructor( receiverId = recipientId, body = body, sentAt = sentAt, - isPending = true + isPending = true, + isRead = true ) ) @@ -143,7 +153,8 @@ internal class MessageRepositoryImpl @Inject constructor( receiverId = myId, body = systemMessage, sentAt = System.currentTimeMillis(), - isPending = false + isPending = false, + isRead = true ) ) } @@ -212,7 +223,8 @@ internal class MessageRepositoryImpl @Inject constructor( receiverId = msg.senderId, body = systemMessage, sentAt = System.currentTimeMillis(), - isPending = false + isPending = false, + isRead = true ) ) } @@ -313,6 +325,27 @@ internal class MessageRepositoryImpl @Inject constructor( } } + override val totalUnreadCount: Flow + get() = settingsRepository.observeString(StringKey.AuthUserId) + .flatMapLatest { myId -> + if (myId.isBlank()) flowOf(0) + else dao.getTotalUnreadCount(myId) + } + + override fun getUnreadCountsBySender(): Flow> = + settingsRepository.observeString(StringKey.AuthUserId) + .flatMapLatest { myId -> + if (myId.isBlank()) flowOf(emptyMap()) + else dao.getUnreadCountsBySender(myId).map { list -> + list.associate { it.senderId to it.count } + } + } + + override suspend fun markConversationAsRead(senderId: String) { + val myId = settingsRepository.getStringOrNull(StringKey.AuthUserId) ?: return + dao.markConversationAsRead(myId = myId, senderId = senderId) + } + private companion object { const val TAG = "MessageRepository" const val SYSTEM_SENDER_USERNAME = "⚙️ System" diff --git a/app/src/main/java/de/bollwerk/app/domain/repository/MessageRepository.kt b/app/src/main/java/de/bollwerk/app/domain/repository/MessageRepository.kt index 01dc0dc..c10a945 100644 --- a/app/src/main/java/de/bollwerk/app/domain/repository/MessageRepository.kt +++ b/app/src/main/java/de/bollwerk/app/domain/repository/MessageRepository.kt @@ -10,4 +10,7 @@ internal interface MessageRepository { suspend fun fetchUsers(): Result> suspend fun getMyUserId(): String? suspend fun drainPendingMessages() + val totalUnreadCount: Flow + fun getUnreadCountsBySender(): Flow> + suspend fun markConversationAsRead(senderId: String) } diff --git a/app/src/main/java/de/bollwerk/app/notification/NotificationHelper.kt b/app/src/main/java/de/bollwerk/app/notification/NotificationHelper.kt index 9e25cd0..65a02ac 100644 --- a/app/src/main/java/de/bollwerk/app/notification/NotificationHelper.kt +++ b/app/src/main/java/de/bollwerk/app/notification/NotificationHelper.kt @@ -191,6 +191,42 @@ internal class NotificationHelper @Inject constructor( } } + /// Aktualisiert den Launcher-Badge-Zähler für ungelesene Nachrichten. + fun updateBadgeCount(count: Int) { + val notificationManager = NotificationManagerCompat.from(context) + if (count <= 0) { + notificationManager.cancel(SUMMARY_NOTIFICATION_ID) + return + } + // If per-sender notifications are active, the summary notification is already informative. + val hasSenderNotifications = synchronized(activeSenderNotificationIds) { + activeSenderNotificationIds.isNotEmpty() + } + if (hasSenderNotifications) return + val messagesIntent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + putExtra(EXTRA_OPEN_MESSAGES, true) + } + val pendingIntent = PendingIntent.getActivity( + context, SUMMARY_NOTIFICATION_ID, messagesIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val badgeNotification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification_message) + .setContentTitle("Ungelesene Nachrichten") + .setContentText("$count ungelesene Nachrichten") + .setNumber(count) + .setGroup(GROUP_KEY) + .setGroupSummary(true) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setContentIntent(pendingIntent) + .build() + try { + notificationManager.notify(SUMMARY_NOTIFICATION_ID, badgeNotification) + } catch (_: SecurityException) {} + } + fun cancelAllMessageNotifications() { val notificationManager = NotificationManagerCompat.from(context) val ids = synchronized(activeSenderNotificationIds) { diff --git a/app/src/main/java/de/bollwerk/app/ui/MainScreen.kt b/app/src/main/java/de/bollwerk/app/ui/MainScreen.kt index 8571fdf..769ba79 100644 --- a/app/src/main/java/de/bollwerk/app/ui/MainScreen.kt +++ b/app/src/main/java/de/bollwerk/app/ui/MainScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -107,6 +109,8 @@ internal fun MainScreen(notificationNavigationEvents: SharedFlow 0) { + BadgedBox( + badge = { + Badge { + Text(if (totalUnreadCount > 99) "99+" else totalUnreadCount.toString()) + } + } + ) { + Icon( + imageVector = if (isSelected) { + destination.selectedIcon + } else { + destination.unselectedIcon + }, + contentDescription = destination.label, + tint = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) } - ) + } else { + Icon( + imageVector = if (isSelected) { + destination.selectedIcon + } else { + destination.unselectedIcon + }, + contentDescription = destination.label, + tint = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } } } } diff --git a/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt b/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt index 737a3ff..215c20c 100644 --- a/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt +++ b/app/src/main/java/de/bollwerk/app/ui/MainViewModel.kt @@ -9,12 +9,16 @@ import de.bollwerk.app.data.sync.WebSocketEvent import de.bollwerk.app.domain.AuthEventBus import de.bollwerk.app.domain.model.SettingsKey.StringKey import de.bollwerk.app.domain.repository.ImportExportRepository +import de.bollwerk.app.domain.repository.MessageRepository import de.bollwerk.app.domain.repository.SettingsRepository import de.bollwerk.app.domain.repository.SyncService import de.bollwerk.app.notification.NotificationHelper import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.time.Instant import javax.inject.Inject @@ -26,12 +30,16 @@ internal class MainViewModel @Inject constructor( private val webSocketClient: WebSocketClient, private val settingsRepository: SettingsRepository, private val importExportRepository: ImportExportRepository, - private val notificationHelper: NotificationHelper + private val notificationHelper: NotificationHelper, + private val messageRepository: MessageRepository ) : ViewModel() { private val _navigateToSettings = MutableSharedFlow(extraBufferCapacity = 1) val navigateToSettings: SharedFlow = _navigateToSettings.asSharedFlow() + val totalUnreadCount: StateFlow = messageRepository.totalUnreadCount + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0) + init { connectOnStartup() observeLoginSuccess() diff --git a/app/src/main/java/de/bollwerk/app/ui/messaging/ChatViewModel.kt b/app/src/main/java/de/bollwerk/app/ui/messaging/ChatViewModel.kt index b7f1149..b827718 100644 --- a/app/src/main/java/de/bollwerk/app/ui/messaging/ChatViewModel.kt +++ b/app/src/main/java/de/bollwerk/app/ui/messaging/ChatViewModel.kt @@ -43,6 +43,7 @@ internal class ChatViewModel @Inject constructor( if (myId.isNotEmpty()) { messageRepository.getConversation(myId, recipientId).collect { messages -> _uiState.update { it.copy(messages = messages) } + messageRepository.markConversationAsRead(recipientId) } } } diff --git a/app/src/main/java/de/bollwerk/app/ui/messaging/UserListScreen.kt b/app/src/main/java/de/bollwerk/app/ui/messaging/UserListScreen.kt index 68986d3..6a6b6b1 100644 --- a/app/src/main/java/de/bollwerk/app/ui/messaging/UserListScreen.kt +++ b/app/src/main/java/de/bollwerk/app/ui/messaging/UserListScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Person +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -36,6 +38,7 @@ internal fun UserListScreen( viewModel: UserListViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val unreadCounts by viewModel.unreadCounts.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -66,7 +69,11 @@ internal fun UserListScreen( ) else -> LazyColumn { items(uiState.users) { user -> - UserListItem(user = user, onClick = { onUserClick(user.id, user.username) }) + UserListItem( + user = user, + unreadCount = unreadCounts[user.id] ?: 0, + onClick = { onUserClick(user.id, user.username) } + ) } } } @@ -75,12 +82,17 @@ internal fun UserListScreen( } @Composable -private fun UserListItem(user: UserListItemDto, onClick: () -> Unit) { +private fun UserListItem(user: UserListItemDto, unreadCount: Int, onClick: () -> Unit) { ListItem( headlineContent = { Text(user.username) }, leadingContent = { Icon(imageVector = Icons.Outlined.Person, contentDescription = null) }, + trailingContent = { + if (unreadCount > 0) { + Badge { Text(if (unreadCount > 99) "99+" else unreadCount.toString()) } + } + }, modifier = Modifier.clickable(onClick = onClick) ) HorizontalDivider() diff --git a/app/src/main/java/de/bollwerk/app/ui/messaging/UserListViewModel.kt b/app/src/main/java/de/bollwerk/app/ui/messaging/UserListViewModel.kt index 27a7f60..164faf7 100644 --- a/app/src/main/java/de/bollwerk/app/ui/messaging/UserListViewModel.kt +++ b/app/src/main/java/de/bollwerk/app/ui/messaging/UserListViewModel.kt @@ -6,7 +6,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import de.bollwerk.app.domain.repository.MessageRepository import de.bollwerk.shared.model.UserListItemDto import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject @@ -24,6 +26,9 @@ internal class UserListViewModel @Inject constructor( private val _uiState = MutableStateFlow(UserListUiState(isLoading = true)) val uiState: StateFlow = _uiState + val unreadCounts: StateFlow> = messageRepository.getUnreadCountsBySender() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap()) + init { loadUsers() } diff --git a/app/src/test/java/de/bollwerk/app/data/repository/MessageRepositoryImplTest.kt b/app/src/test/java/de/bollwerk/app/data/repository/MessageRepositoryImplTest.kt index d08726e..0a511d3 100644 --- a/app/src/test/java/de/bollwerk/app/data/repository/MessageRepositoryImplTest.kt +++ b/app/src/test/java/de/bollwerk/app/data/repository/MessageRepositoryImplTest.kt @@ -1,6 +1,7 @@ package de.bollwerk.app.data.repository import de.bollwerk.app.data.db.dao.MessageDao +import de.bollwerk.app.data.db.dao.UnreadCountBySender import de.bollwerk.app.data.db.entity.MessageEntity import de.bollwerk.app.data.security.E2EEKeyManager import de.bollwerk.app.data.sync.WebSocketClient @@ -31,6 +32,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest @@ -45,6 +47,7 @@ import org.junit.Test private class FakeMessageDao : MessageDao { val upserted = mutableListOf() val delivered = mutableListOf() + val markedAsRead = mutableListOf>() // myId to senderId override suspend fun upsert(message: MessageEntity) { upserted.add(message) } override suspend fun insertIfNotExists(message: MessageEntity) { upserted.add(message) } @@ -60,6 +63,11 @@ private class FakeMessageDao : MessageDao { val idx = upserted.indexOfFirst { it.id == id } if (idx >= 0) upserted[idx] = upserted[idx].copy(isPending = false) } + override fun getUnreadCountsBySender(myId: String): Flow> = flowOf(emptyList()) + override fun getTotalUnreadCount(myId: String): Flow = flowOf(0) + override suspend fun markConversationAsRead(myId: String, senderId: String) { + markedAsRead.add(myId to senderId) + } } private class FakeMessageSettingsRepository : SettingsRepository { @@ -277,6 +285,31 @@ class MessageRepositoryImplTest { // Then – decryptMessage is mocked to return plaintext as-is assertTrue(dao.upserted.any { it.id == "m1" && !it.isPending }) + assertTrue("New incoming message must be stored as unread", + dao.upserted.any { it.id == "m1" && !it.isRead }) + } + + @Test + fun test_markConversationAsRead_delegatesToDao() = runTest { + // Given + val dao = FakeMessageDao() + val settings = FakeMessageSettingsRepository().apply { + set(SettingsKeys.AUTH_USER_ID, "user1") + } + val repo = buildRepository( + dao = dao, + httpClient = createClient(MockEngine { respondError(HttpStatusCode.ServiceUnavailable) }), + settings = settings + ) + + // When + repo.markConversationAsRead(senderId = "user2") + + // Then + assertTrue( + "markConversationAsRead must forward myId and senderId to DAO", + dao.markedAsRead.contains("user1" to "user2") + ) } @Test diff --git a/app/src/test/java/de/bollwerk/app/ui/messaging/ChatViewModelTest.kt b/app/src/test/java/de/bollwerk/app/ui/messaging/ChatViewModelTest.kt index 96d59b9..f4af3cc 100644 --- a/app/src/test/java/de/bollwerk/app/ui/messaging/ChatViewModelTest.kt +++ b/app/src/test/java/de/bollwerk/app/ui/messaging/ChatViewModelTest.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -29,7 +30,10 @@ private class FakeChatMessageRepository( var sendMessageCalled = false var lastSentBody: String? = null + override val totalUnreadCount: Flow = flowOf(0) override fun getConversation(myId: String, otherId: String): Flow> = conversation + override fun getUnreadCountsBySender(): Flow> = flowOf(emptyMap()) + override suspend fun markConversationAsRead(senderId: String) = Unit override suspend fun sendMessage(recipientId: String, body: String) { sendMessageCalled = true lastSentBody = body diff --git a/app/src/test/java/de/bollwerk/app/ui/messaging/UserListViewModelTest.kt b/app/src/test/java/de/bollwerk/app/ui/messaging/UserListViewModelTest.kt index f700f31..1d2edd8 100644 --- a/app/src/test/java/de/bollwerk/app/ui/messaging/UserListViewModelTest.kt +++ b/app/src/test/java/de/bollwerk/app/ui/messaging/UserListViewModelTest.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -27,8 +28,11 @@ private class FakeUserListMessageRepository( ) : MessageRepository { var fetchCount = 0 + override val totalUnreadCount: Flow = flowOf(0) override fun getConversation(myId: String, otherId: String): Flow> = MutableStateFlow(emptyList()) + override fun getUnreadCountsBySender(): Flow> = flowOf(emptyMap()) + override suspend fun markConversationAsRead(senderId: String) = Unit override suspend fun sendMessage(recipientId: String, body: String) = Unit override suspend fun fetchUsers(): Result> { fetchCount++