Skip to content

Commit

Permalink
feat: archived conversation list [WPB-4429] (#2109)
Browse files Browse the repository at this point in the history
* feat: archived conversation list

* detekt fix

* renamed variable
  • Loading branch information
Garzas authored Oct 4, 2023
1 parent 5bb647a commit 751cd75
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Instant

Expand Down Expand Up @@ -112,7 +113,7 @@ interface ConversationRepository {

suspend fun getConversationList(): Either<StorageFailure, Flow<List<Conversation>>>
suspend fun observeConversationList(): Flow<List<Conversation>>
suspend fun observeConversationListDetails(includeArchived: Boolean): Flow<List<ConversationDetails>>
suspend fun observeConversationListDetails(fromArchive: Boolean): Flow<List<ConversationDetails>>
suspend fun observeConversationDetailsById(conversationID: ConversationId): Flow<Either<StorageFailure, ConversationDetails>>
suspend fun fetchConversation(conversationID: ConversationId): Either<CoreFailure, Unit>
suspend fun fetchSentConnectionConversation(conversationID: ConversationId): Either<CoreFailure, Unit>
Expand Down Expand Up @@ -410,10 +411,10 @@ internal class ConversationDataSource internal constructor(
return conversationDAO.getAllConversations().map { it.map(conversationMapper::fromDaoModel) }
}

override suspend fun observeConversationListDetails(includeArchived: Boolean): Flow<List<ConversationDetails>> =
override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow<List<ConversationDetails>> =
combine(
conversationDAO.getAllConversationDetails(includeArchived),
messageDAO.observeLastMessages(),
conversationDAO.getAllConversationDetails(fromArchive),
if (fromArchive) flowOf(listOf()) else messageDAO.observeLastMessages(),
messageDAO.observeConversationsUnreadEvents(),
) { conversationList, lastMessageList, unreadEvents ->
val lastMessageMap = lastMessageList.associateBy { it.conversationId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ import kotlinx.coroutines.flow.Flow
* @see ConversationDetails
*/
fun interface ObserveConversationListDetailsUseCase {
suspend operator fun invoke(includeArchived: Boolean): Flow<List<ConversationDetails>>
suspend operator fun invoke(fromArchive: Boolean): Flow<List<ConversationDetails>>
}

internal class ObserveConversationListDetailsUseCaseImpl(
private val conversationRepository: ConversationRepository,
) : ObserveConversationListDetailsUseCase {

override suspend operator fun invoke(includeArchived: Boolean): Flow<List<ConversationDetails>> =
conversationRepository.observeConversationListDetails(includeArchived)
override suspend operator fun invoke(fromArchive: Boolean): Flow<List<ConversationDetails>> {
return conversationRepository.observeConversationListDetails(fromArchive)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO
import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity
import com.wire.kalium.persistence.dao.message.MessageDAO
import com.wire.kalium.persistence.dao.message.MessageEntity
import com.wire.kalium.persistence.dao.message.MessagePreviewEntity
import com.wire.kalium.persistence.dao.message.MessagePreviewEntityContent
import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity
import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity
import com.wire.kalium.util.DateTimeUtil
Expand Down Expand Up @@ -664,11 +666,12 @@ class ConversationRepositoryTest {
}

@Test
fun givenAGroupConversationHasNewMessages_whenGettingConversationDetails_ThenCorrectlyGetUnreadMessageCount() = runTest {
fun givenAGroupConversationHasNewMessages_whenGettingConversationDetails_ThenCorrectlyGetUnreadMessageCountAndLastMessage() = runTest {
// given
val conversationIdEntity = ConversationIDEntity("some_value", "some_domain")
val conversationId = QualifiedID("some_value", "some_domain")
val shouldFetchArchivedConversations = false
val shouldFetchFromArchivedConversations = false
val messagePreviewEntity = MESSAGE_PREVIEW_ENTITY.copy(conversationId = conversationIdEntity)

val conversationEntity = TestConversation.VIEW_ENTITY.copy(
id = conversationIdEntity,
Expand All @@ -683,24 +686,68 @@ class ConversationRepositoryTest {

val (_, conversationRepository) = Arrangement()
.withConversations(listOf(conversationEntity))
.withLastMessages(listOf())
.withLastMessages(listOf(messagePreviewEntity))
.withConversationUnreadEvents(listOf(conversationUnreadEventEntity))
.arrange()

// when
conversationRepository.observeConversationListDetails(shouldFetchArchivedConversations).test {
conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test {
val result = awaitItem()

assertContains(result.map { it.conversation.id }, conversationId)
val conversation = result.first { it.conversation.id == conversationId }

assertIs<ConversationDetails.Group>(conversation)
assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount)
assertEquals(
MapperProvider.messageMapper(TestUser.SELF.id).fromEntityToMessagePreview(messagePreviewEntity),
conversation.lastMessage
)

awaitComplete()
}
}

@Test
fun givenArchivedConversationHasNewMessages_whenGettingConversationDetails_ThenCorrectlyGetUnreadMessageCountAndNullLastMessage() =
runTest {
// given
val conversationIdEntity = ConversationIDEntity("some_value", "some_domain")
val conversationId = QualifiedID("some_value", "some_domain")
val shouldFetchFromArchivedConversations = true

val conversationEntity = TestConversation.VIEW_ENTITY.copy(
id = conversationIdEntity,
type = ConversationEntity.Type.GROUP,
)

val unreadMessagesCount = 5
val conversationUnreadEventEntity = ConversationUnreadEventEntity(
conversationIdEntity,
mapOf(UnreadEventTypeEntity.MESSAGE to unreadMessagesCount)
)

val (_, conversationRepository) = Arrangement()
.withConversations(listOf(conversationEntity))
.withLastMessages(listOf(MESSAGE_PREVIEW_ENTITY.copy(conversationId = conversationIdEntity)))
.withConversationUnreadEvents(listOf(conversationUnreadEventEntity))
.arrange()

// when
conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test {
val result = awaitItem()

assertContains(result.map { it.conversation.id }, conversationId)
val conversation = result.first { it.conversation.id == conversationId }

assertIs<ConversationDetails.Group>(conversation)
assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount)
assertEquals(null, conversation.lastMessage)

awaitComplete()
}
}

@Test
fun givenAGroupConversationHasNotNewMessages_whenGettingConversationDetails_ThenReturnZeroUnreadMessageCount() = runTest {
// given
Expand Down Expand Up @@ -753,7 +800,7 @@ class ConversationRepositoryTest {
// given
val conversationIdEntity = ConversationIDEntity("some_value", "some_domain")
val conversationId = QualifiedID("some_value", "some_domain")
val shouldFetchArchivedConversations = false
val shouldFetchFromArchivedConversations = false

val conversationEntity = TestConversation.VIEW_ENTITY.copy(
id = conversationIdEntity, type = ConversationEntity.Type.ONE_ON_ONE,
Expand All @@ -773,7 +820,7 @@ class ConversationRepositoryTest {
.arrange()

// when
conversationRepository.observeConversationListDetails(shouldFetchArchivedConversations).test {
conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test {
val result = awaitItem()

assertContains(result.map { it.conversation.id }, conversationId)
Expand Down Expand Up @@ -1487,7 +1534,16 @@ class ConversationRepositoryTest {
conversationsFound = listOf(CONVERSATION_RESPONSE),
conversationsFailed = listOf(ConversationIdDTO("failedId", "someDomain")),
conversationsNotFound = emptyList()
)

val MESSAGE_PREVIEW_ENTITY = MessagePreviewEntity(
id = "some_id",
conversationId = CONVERSATION_ENTITY_ID,
content = MessagePreviewEntityContent.Text("sender", "Hey"),
date = "2022-03-30T15:36:00.000Z",
visibility = MessageEntity.Visibility.VISIBLE,
isSelfMessage = false,
senderUserId = USER_ENTITY_ID
)

private val TEST_QUALIFIED_ID_ENTITY = PersistenceQualifiedId("value", "domain")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ WHERE
OR (type IS 'ONE_ON_ONE' AND userDeleted = 1) -- show deleted 1:1 convos, to maintain prev, logic
)
AND (protocol IS 'PROTEUS' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED'))
AND (archived = 0 OR archived = :isArchived)
AND archived = :fromArchive
ORDER BY lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC;

selectAllConversations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface ConversationDAO {
suspend fun updateConversationReadDate(conversationID: QualifiedIDEntity, date: Instant)
suspend fun updateAllConversationsNotificationDate()
suspend fun getAllConversations(): Flow<List<ConversationViewEntity>>
suspend fun getAllConversationDetails(includeArchived: Boolean): Flow<List<ConversationViewEntity>>
suspend fun getAllConversationDetails(fromArchive: Boolean): Flow<List<ConversationViewEntity>>
suspend fun observeGetConversationByQualifiedID(qualifiedID: QualifiedIDEntity): Flow<ConversationViewEntity?>
suspend fun observeGetConversationBaseInfoByQualifiedID(qualifiedID: QualifiedIDEntity): Flow<ConversationEntity?>
suspend fun getConversationBaseInfoByQualifiedID(qualifiedID: QualifiedIDEntity): ConversationEntity?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ internal class ConversationDAOImpl internal constructor(
.map { it.map(conversationMapper::toModel) }
}

override suspend fun getAllConversationDetails(includeArchived: Boolean): Flow<List<ConversationViewEntity>> {
return conversationQueries.selectAllConversationDetails(includeArchived)
override suspend fun getAllConversationDetails(fromArchive: Boolean): Flow<List<ConversationViewEntity>> {
return conversationQueries.selectAllConversationDetails(fromArchive)
.asFlow()
.mapToList()
.flowOn(coroutineContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.wire.kalium.persistence.dao.asset.AssetDAO
import com.wire.kalium.persistence.dao.asset.AssetEntity
import com.wire.kalium.persistence.dao.conversation.ConversationDAO
import com.wire.kalium.persistence.dao.conversation.ConversationEntity
import com.wire.kalium.persistence.dao.conversation.ConversationMapper
import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity
import com.wire.kalium.persistence.dao.conversation.MLS_DEFAULT_LAST_KEY_MATERIAL_UPDATE_MILLI
import com.wire.kalium.persistence.dao.conversation.ProposalTimerEntity
Expand Down Expand Up @@ -794,7 +793,7 @@ class ConversationDAOTest : BaseDatabaseTest() {

@Test
fun givenConnectionRequestAndUserWithName_whenSelectingAllConversationDetails_thenShouldReturnConnectionRequest() = runTest {
val includeArchived = false
val fromArchive = false
val conversationId = QualifiedIDEntity("connection-conversationId", "domain")
val conversation = conversationEntity1.copy(id = conversationId, type = ConversationEntity.Type.CONNECTION_PENDING)
val connectionEntity = ConnectionEntity(
Expand All @@ -811,7 +810,7 @@ class ConversationDAOTest : BaseDatabaseTest() {
conversationDAO.insertConversation(conversation)
connectionDAO.insertConnection(connectionEntity)

conversationDAO.getAllConversationDetails(includeArchived).first().let {
conversationDAO.getAllConversationDetails(fromArchive).first().let {
assertEquals(1, it.size)
val result = it.first()

Expand All @@ -822,7 +821,7 @@ class ConversationDAOTest : BaseDatabaseTest() {

@Test
fun givenConnectionRequestAndUserWithoutName_whenSelectingAllConversationDetails_thenShouldNotReturnConnectionRequest() = runTest {
val includeArchived = false
val fromArchive = false
val conversationId = QualifiedIDEntity("connection-conversationId", "domain")
val conversation = conversationEntity1.copy(id = conversationId, type = ConversationEntity.Type.CONNECTION_PENDING)
val connectionEntity = ConnectionEntity(
Expand All @@ -839,14 +838,14 @@ class ConversationDAOTest : BaseDatabaseTest() {
conversationDAO.insertConversation(conversation)
connectionDAO.insertConnection(connectionEntity)

conversationDAO.getAllConversationDetails(includeArchived).first().let {
conversationDAO.getAllConversationDetails(fromArchive).first().let {
assertEquals(0, it.size)
}
}

@Test
fun givenLocalConversations_whenGettingAllConversations_thenShouldReturnsOnlyConversationsWithMetadata() = runTest {
val includeArchived = false
val fromArchive = false
conversationDAO.insertConversation(conversationEntity1)
conversationDAO.insertConversation(conversationEntity2)

Expand All @@ -856,40 +855,58 @@ class ConversationDAOTest : BaseDatabaseTest() {
memberDAO.insertMember(member1, conversationEntity1.id)
memberDAO.insertMember(member2, conversationEntity1.id)

conversationDAO.getAllConversationDetails(includeArchived).first().let {
conversationDAO.getAllConversationDetails(fromArchive).first().let {
assertEquals(1, it.size)
assertEquals(conversationEntity1.id, it.first().id)
}
}

@Test
fun givenLocalConversations_whenGettingAllArchivedAndNotArchivedConversations_thenShouldReturnThemAll() = runTest {
val includeArchived = true
fun givenLocalConversations_whenGettingArchivedConversations_thenShouldReturnOnlyArchived() = runTest {
val fromArchive = true
conversationDAO.insertConversation(conversationEntity1.copy(archived = true))
conversationDAO.insertConversation(conversationEntity2.copy(archived = false))
conversationDAO.insertConversation(conversationEntity2.copy(archived = true))
conversationDAO.insertConversation(conversationEntity3.copy(archived = false))

userDAO.insertUser(user1)
userDAO.insertUser(user2)

memberDAO.insertMember(member1, conversationEntity1.id)
memberDAO.insertMember(member2, conversationEntity2.id)

val result = conversationDAO.getAllConversationDetails(includeArchived).first()
val result = conversationDAO.getAllConversationDetails(fromArchive).first()

assertEquals(2, result.size)
}

@Test
fun givenLocalConversations_whenGettingNotArchivedConversations_thenShouldReturnOnlyNotArchived() = runTest {
val fromArchive = false
conversationDAO.insertConversation(conversationEntity1.copy(archived = true))
conversationDAO.insertConversation(conversationEntity1.copy(archived = false))
conversationDAO.insertConversation(conversationEntity2.copy(archived = false))

userDAO.insertUser(user1)
userDAO.insertUser(user2)

memberDAO.insertMember(member1, conversationEntity1.id)
memberDAO.insertMember(member2, conversationEntity2.id)

val result = conversationDAO.getAllConversationDetails(fromArchive).first()

assertEquals(2, result.size)
}

@Test
fun givenObserveConversationList_whenAConversationHaveNullAsName_thenItIsIncluded() = runTest {
// given
val includeArchived = false
val fromArchive = false
val conversation = conversationEntity1.copy(name = null, type = ConversationEntity.Type.GROUP, hasIncompleteMetadata = false)
conversationDAO.insertConversation(conversation)
insertTeamUserAndMember(team, user1, conversation.id)

// when
val result = conversationDAO.getAllConversationDetails(includeArchived).first()
val result = conversationDAO.getAllConversationDetails(fromArchive).first()

// then
assertEquals(conversation.toViewEntity(user1), result.firstOrNull { it.id == conversation.id })
Expand All @@ -898,13 +915,13 @@ class ConversationDAOTest : BaseDatabaseTest() {
@Test
fun givenObserveConversationList_whenAConversationHaveIncompleteMetadata_thenItIsNotIncluded() = runTest {
// given
val includeArchived = false
val fromArchive = false
val conversation = conversationEntity1.copy(hasIncompleteMetadata = true)
conversationDAO.insertConversation(conversation)
insertTeamUserAndMember(team, user1, conversation.id)

// when
val result = conversationDAO.getAllConversationDetails(includeArchived).first()
val result = conversationDAO.getAllConversationDetails(fromArchive).first()

// then
assertNull(result.firstOrNull { it.id == conversation.id })
Expand All @@ -913,7 +930,7 @@ class ConversationDAOTest : BaseDatabaseTest() {
@Test
fun givenConversations_whenObservingTheFullList_thenConvWithNullNameAreLast() = runTest {
// given
val includeArchived = false
val fromArchive = false
val conversation1 = conversationEntity1.copy(
id = ConversationIDEntity("convNullName", "domain"),
name = null,
Expand All @@ -935,7 +952,7 @@ class ConversationDAOTest : BaseDatabaseTest() {
insertTeamUserAndMember(team, user1, conversation2.id)

// when
val result = conversationDAO.getAllConversationDetails(includeArchived).first()
val result = conversationDAO.getAllConversationDetails(fromArchive).first()

// then
assertEquals(conversation2.id, result[0].id)
Expand All @@ -945,7 +962,7 @@ class ConversationDAOTest : BaseDatabaseTest() {
@Test
fun givenArchivedConversations_whenObservingTheFullListWithNoArchived_thenReturnedConversationsShouldNotBeArchived() = runTest {
// given
val includeArchived = false
val fromArchive = false
val conversation1 = conversationEntity1.copy(
id = ConversationIDEntity("convNullName", "domain"),
name = null,
Expand All @@ -969,7 +986,7 @@ class ConversationDAOTest : BaseDatabaseTest() {
insertTeamUserAndMember(team, user1, conversation2.id)

// when
val result = conversationDAO.getAllConversationDetails(includeArchived).first()
val result = conversationDAO.getAllConversationDetails(fromArchive).first()

// then
assertTrue(result.size == 1)
Expand Down

0 comments on commit 751cd75

Please sign in to comment.