Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import com.wire.kalium.logic.feature.conversation.UpdateConversationAccessRoleUs
import com.wire.kalium.logic.feature.conversation.UpdateConversationArchivedStatusUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationMemberRoleUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationMutedStatusUseCase
import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReceiptModeUseCase
import com.wire.kalium.logic.feature.conversation.apps.ChangeAccessForAppsInConversationUseCase
Expand Down Expand Up @@ -187,6 +188,11 @@ class ConversationModule {
fun provideUpdateConversationReadDateUseCase(conversationScope: ConversationScope): UpdateConversationReadDateUseCase =
conversationScope.updateConversationReadDateUseCase

@ViewModelScoped
@Provides
fun provideMarkConversationAsReadLocallyUseCase(conversationScope: ConversationScope): MarkConversationAsReadLocallyUseCase =
conversationScope.markConversationAsReadLocally

@ViewModelScoped
@Provides
fun provideUpdateConversationAccessUseCase(conversationScope: ConversationScope): UpdateConversationAccessRoleUseCase =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -665,6 +666,15 @@ fun ConversationScreen(
isWireCellsEnabled = conversationInfoViewModel.conversationInfoViewState.isWireCellEnabled,
)
BackHandler { conversationScreenOnBackButtonClick(messageComposerViewModel, messageComposerStateHolder, navigator) }

// Mark conversation as read when leaving, regardless of how the user exits
// (back button, system gesture, navigation to another screen, etc.)
DisposableEffect(messageComposerViewModel) {
onDispose {
messageComposerViewModel.onConversationClosed()
}
}

DeleteMessageDialog(
dialogState = conversationMessagesViewModel.deleteMessageDialogState,
deleteMessage = conversationMessagesViewModel::deleteMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase
import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult
import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase
import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase
import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase
import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase
Expand All @@ -55,6 +56,7 @@ import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
Expand All @@ -74,6 +76,7 @@ class MessageComposerViewModel @Inject constructor(
private val isFileSharingEnabled: IsFileSharingEnabledUseCase,
private val observeConversationInteractionAvailability: ObserveConversationInteractionAvailabilityUseCase,
private val updateConversationReadDate: UpdateConversationReadDateUseCase,
private val markConversationAsReadLocally: MarkConversationAsReadLocallyUseCase,
private val contactMapper: ContactMapper,
private val membersToMention: MembersToMentionUseCase,
private val enqueueMessageSelfDeletion: EnqueueMessageSelfDeletionUseCase,
Expand Down Expand Up @@ -106,6 +109,8 @@ class MessageComposerViewModel @Inject constructor(
InvalidLinkDialogState.Hidden
)

private var lastReadInstant: Instant? = null

init {
initTempWritableVideoUri()
initTempWritableImageUri()
Expand Down Expand Up @@ -195,8 +200,24 @@ class MessageComposerViewModel @Inject constructor(
}

fun updateConversationReadDate(utcISO: String) {
viewModelScope.launch(dispatchers.io()) {
updateConversationReadDate(conversationId, Instant.parse(utcISO))
val instant = Instant.parse(utcISO)
lastReadInstant = instant
viewModelScope.launch(NonCancellable) {
updateConversationReadDate(conversationId, instant)
}
}

/**
* Called when the user leaves the conversation.
* Immediately updates the local read date to clear unread badges
* without waiting for the debounced update to complete.
* If the user viewed messages, uses the last read timestamp.
*/
fun onConversationClosed() {
lastReadInstant?.let { instant ->
viewModelScope.launch(NonCancellable) {
markConversationAsReadLocally(conversationId, instant)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase
import com.wire.kalium.logic.feature.conversation.IsInteractionAvailableResult
import com.wire.kalium.logic.feature.conversation.MembersToMentionUseCase
import com.wire.kalium.logic.feature.conversation.ObserveConversationInteractionAvailabilityUseCase
import com.wire.kalium.logic.feature.conversation.MarkConversationAsReadLocallyUseCase
import com.wire.kalium.logic.feature.conversation.SendTypingEventUseCase
import com.wire.kalium.logic.feature.conversation.UpdateConversationReadDateUseCase
import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletionUseCase
import com.wire.kalium.logic.feature.selfDeletingMessages.PersistNewSelfDeletionTimerUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionFlowUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.user.IsFileSharingEnabledUseCase
import com.wire.kalium.common.functional.Either
import com.wire.kalium.logic.sync.ObserveSyncStateUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
Expand Down Expand Up @@ -100,6 +102,7 @@ internal class MessageComposerViewModelArrangement {
} returns flowOf(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.USER_ID)))
coEvery { globalDataStore.enterToSendFlow() } returns flowOf(false)
coEvery { observeEstablishedCalls() } returns emptyFlow()
coEvery { markConversationAsReadLocallyUseCase(any(), any()) } returns Either.Right(false)
}

@MockK
Expand All @@ -120,6 +123,9 @@ internal class MessageComposerViewModelArrangement {
@MockK
private lateinit var updateConversationReadDateUseCase: UpdateConversationReadDateUseCase

@MockK
lateinit var markConversationAsReadLocallyUseCase: MarkConversationAsReadLocallyUseCase

@MockK
private lateinit var observeSyncState: ObserveSyncStateUseCase

Expand Down Expand Up @@ -158,6 +164,7 @@ internal class MessageComposerViewModelArrangement {
dispatchers = TestDispatcherProvider(),
isFileSharingEnabled = isFileSharingEnabledUseCase,
updateConversationReadDate = updateConversationReadDateUseCase,
markConversationAsReadLocally = markConversationAsReadLocallyUseCase,
observeConversationInteractionAvailability = observeConversationInteractionAvailabilityUseCase,
contactMapper = contactMapper,
membersToMention = membersToMention,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -93,4 +94,72 @@ class MessageComposerViewModelTest {
// then
assertTrue(!viewModel.messageComposerViewState.value.enterToSend)
}

@Test
fun `given messages were viewed, when conversation is closed, then mark as read with last viewed timestamp`() = runTest {
// given
val (arrangement, viewModel) = MessageComposerViewModelArrangement()
.withSuccessfulViewModelInit()
.arrange()
val timestamp = "2024-01-15T10:30:00Z"
val expectedInstant = Instant.parse(timestamp)

// when
viewModel.updateConversationReadDate(timestamp)
advanceUntilIdle()
viewModel.onConversationClosed()
advanceUntilIdle()

// then
coVerify(exactly = 1) {
arrangement.markConversationAsReadLocallyUseCase(
arrangement.conversationId,
expectedInstant
)
}
}

@Test
fun `given no messages were viewed, when conversation is closed, then mark as read is not called`() = runTest {
// given
val (arrangement, viewModel) = MessageComposerViewModelArrangement()
.withSuccessfulViewModelInit()
.arrange()
advanceUntilIdle()

// when - close without calling updateConversationReadDate
viewModel.onConversationClosed()
advanceUntilIdle()

// then
coVerify(exactly = 0) {
arrangement.markConversationAsReadLocallyUseCase(any(), any())
}
}

@Test
fun `given multiple read date updates, when conversation is closed, then use the most recent timestamp`() = runTest {
// given
val (arrangement, viewModel) = MessageComposerViewModelArrangement()
.withSuccessfulViewModelInit()
.arrange()
val olderTimestamp = "2024-01-15T10:00:00Z"
val newerTimestamp = "2024-01-15T10:30:00Z"
val expectedInstant = Instant.parse(newerTimestamp)

// when
viewModel.updateConversationReadDate(olderTimestamp)
viewModel.updateConversationReadDate(newerTimestamp)
advanceUntilIdle()
viewModel.onConversationClosed()
advanceUntilIdle()

// then
coVerify(exactly = 1) {
arrangement.markConversationAsReadLocallyUseCase(
arrangement.conversationId,
expectedInstant
)
}
}
}
2 changes: 1 addition & 1 deletion kalium
Submodule kalium updated 26 files
+1 −1 buildSrc/src/main/kotlin/scripts/detekt.gradle.kts
+3 −3 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ConnectionDAOImpl.kt
+4 −0 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/MetadataDAOImpl.kt
+9 −3 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/ServiceDAO.kt
+1 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/TeamDAOImpl.kt
+9 −7 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt
+1 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/asset/AssetDAOImpl.kt
+7 −7 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/call/CallDAOImpl.kt
+2 −2 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/client/ClientDAOImpl.kt
+6 −5 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt
+2 −2 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/event/EventDAOImpl.kt
+2 −2 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt
+16 −9 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt
+4 −1 ...rsistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/attachment/MessageAttachmentsDao.kt
+1 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt
+1 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/newclient/NewClientDAOImpl.kt
+1 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionDAO.kt
+1 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/ReceiptDAO.kt
+7 −5 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/AccountsDAO.kt
+11 −3 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt
+2 −1 data/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt
+3 −0 detekt/detekt.yml
+1 −1 gradle/libs.versions.toml
+3 −0 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt
+59 −0 logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/MarkConversationAsReadLocallyUseCase.kt
+25 −3 scripts/bootstrap.mk