feat(messaging): ungelesene Nachrichten als Badges anzeigen (#110)
- 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)
This commit is contained in:
parent
06fa017c04
commit
512829dd49
14 changed files with 208 additions and 22 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<List<UnreadCountBySender>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM messages WHERE receiver_id = :myId AND is_read = 0")
|
||||
fun getTotalUnreadCount(myId: String): Flow<Int>
|
||||
|
||||
@Query("UPDATE messages SET is_read = 1 WHERE receiver_id = :myId AND sender_id = :senderId")
|
||||
suspend fun markConversationAsRead(myId: String, senderId: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<List<MessageEntity>> =
|
||||
|
|
@ -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<Int>
|
||||
get() = settingsRepository.observeString(StringKey.AuthUserId)
|
||||
.flatMapLatest { myId ->
|
||||
if (myId.isBlank()) flowOf(0)
|
||||
else dao.getTotalUnreadCount(myId)
|
||||
}
|
||||
|
||||
override fun getUnreadCountsBySender(): Flow<Map<String, Int>> =
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -10,4 +10,7 @@ internal interface MessageRepository {
|
|||
suspend fun fetchUsers(): Result<List<UserListItemDto>>
|
||||
suspend fun getMyUserId(): String?
|
||||
suspend fun drainPendingMessages()
|
||||
val totalUnreadCount: Flow<Int>
|
||||
fun getUnreadCountsBySender(): Flow<Map<String, Int>>
|
||||
suspend fun markConversationAsRead(senderId: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<NotificationNav
|
|||
val updateViewModel: UpdateViewModel = hiltViewModel()
|
||||
val updateState by updateViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
val totalUnreadCount by mainViewModel.totalUnreadCount.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (showTopBar) {
|
||||
|
|
@ -153,19 +157,43 @@ internal fun MainScreen(notificationNavigationEvents: SharedFlow<NotificationNav
|
|||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isSelected) {
|
||||
destination.selectedIcon
|
||||
} else {
|
||||
destination.unselectedIcon
|
||||
},
|
||||
contentDescription = destination.label,
|
||||
tint = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
if (destination == TopLevelDestination.MESSAGES && totalUnreadCount > 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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Unit>(extraBufferCapacity = 1)
|
||||
val navigateToSettings: SharedFlow<Unit> = _navigateToSettings.asSharedFlow()
|
||||
|
||||
val totalUnreadCount: StateFlow<Int> = messageRepository.totalUnreadCount
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
||||
|
||||
init {
|
||||
connectOnStartup()
|
||||
observeLoginSuccess()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<UserListUiState> = _uiState
|
||||
|
||||
val unreadCounts: StateFlow<Map<String, Int>> = messageRepository.getUnreadCountsBySender()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||
|
||||
init {
|
||||
loadUsers()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<MessageEntity>()
|
||||
val delivered = mutableListOf<String>()
|
||||
val markedAsRead = mutableListOf<Pair<String, String>>() // 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<List<UnreadCountBySender>> = flowOf(emptyList())
|
||||
override fun getTotalUnreadCount(myId: String): Flow<Int> = 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
|
||||
|
|
|
|||
|
|
@ -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<Int> = flowOf(0)
|
||||
override fun getConversation(myId: String, otherId: String): Flow<List<MessageEntity>> = conversation
|
||||
override fun getUnreadCountsBySender(): Flow<Map<String, Int>> = flowOf(emptyMap())
|
||||
override suspend fun markConversationAsRead(senderId: String) = Unit
|
||||
override suspend fun sendMessage(recipientId: String, body: String) {
|
||||
sendMessageCalled = true
|
||||
lastSentBody = body
|
||||
|
|
|
|||
|
|
@ -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<Int> = flowOf(0)
|
||||
override fun getConversation(myId: String, otherId: String): Flow<List<MessageEntity>> =
|
||||
MutableStateFlow(emptyList())
|
||||
override fun getUnreadCountsBySender(): Flow<Map<String, Int>> = flowOf(emptyMap())
|
||||
override suspend fun markConversationAsRead(senderId: String) = Unit
|
||||
override suspend fun sendMessage(recipientId: String, body: String) = Unit
|
||||
override suspend fun fetchUsers(): Result<List<UserListItemDto>> {
|
||||
fetchCount++
|
||||
|
|
|
|||
Loading…
Reference in a new issue