RoomList invites : update and remove tests

This commit is contained in:
ganfra 2024-04-16 15:45:57 +02:00
parent 8f583d78ac
commit b09269333d
24 changed files with 144 additions and 584 deletions

View file

@ -50,7 +50,6 @@ dependencies {
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)

View file

@ -1,265 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.invite.impl.invitelist
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.invite.test.FakeSeenInvitesStore
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class InviteListPresenterTests {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - starts empty, adds invites when received`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createInviteListPresenter(
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.inviteList).isEmpty()
roomListService.postInviteRooms(listOf(aRoomSummary()))
val withInviteState = awaitItem()
assertThat(withInviteState.inviteList.size).isEqualTo(1)
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
}
}
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = createInviteListPresenter(
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val withInviteState = awaitInitialItem()
assertThat(withInviteState.inviteList.size).isEqualTo(1)
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
AvatarData(
id = A_USER_ID.value,
name = A_USER_NAME,
url = AN_AVATAR_URL,
size = AvatarSize.RoomInviteItem,
)
)
assertThat(withInviteState.inviteList[0].sender).isNull()
}
}
@Test
fun `present - includes sender details for room invites`() = runTest {
val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createInviteListPresenter(
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val withInviteState = awaitInitialItem()
assertThat(withInviteState.inviteList.size).isEqualTo(1)
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
AvatarData(
id = A_USER_ID.value,
name = A_USER_NAME,
url = AN_AVATAR_URL,
size = AvatarSize.InviteSender,
)
)
}
}
@Test
fun `present - stores seen invites when received`() = runTest {
val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore()
val presenter = createInviteListPresenter(
FakeMatrixClient(
roomListService = roomListService,
),
store,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem()
// When one invite is received, that ID is saved
roomListService.postInviteRooms(listOf(aRoomSummary()))
awaitItem()
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
// When a second is added, both are saved
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
awaitItem()
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
// When they're both dismissed, an empty set is saved
roomListService.postInviteRooms(listOf())
awaitItem()
assertThat(store.getProvidedRoomIds()).isEmpty()
}
}
@Test
fun `present - marks invite as new if they're unseen`() = runTest {
val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore()
store.publishRoomIds(setOf(A_ROOM_ID))
val presenter = createInviteListPresenter(
FakeMatrixClient(
roomListService = roomListService,
),
store,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem()
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
skipItems(1)
val withInviteState = awaitItem()
assertThat(withInviteState.inviteList.size).isEqualTo(2)
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
assertThat(withInviteState.inviteList[0].isNew).isFalse()
assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
assertThat(withInviteState.inviteList[1].isNew).isTrue()
}
}
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
postInviteRooms(
listOf(
RoomSummary.Filled(
aRoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
avatarUrl = null,
isDirect = false,
lastMessage = null,
inviter = aRoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
)
)
)
)
return this
}
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
postInviteRooms(
listOf(
RoomSummary.Filled(
aRoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
avatarUrl = null,
isDirect = true,
lastMessage = null,
inviter = aRoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
)
)
)
)
return this
}
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
aRoomSummaryDetails(
roomId = id,
name = A_ROOM_NAME,
avatarUrl = null,
isDirect = false,
lastMessage = null,
)
)
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
skipItems(1)
return awaitItem()
}
private fun createInviteListPresenter(
client: MatrixClient,
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
) = InviteListPresenter(
client,
seenInvitesStore,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
)
}

View file

@ -1,29 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.invite.test"
}
dependencies {
implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api)
api(projects.features.invite.api)
}

View file

@ -1,38 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.invite.test
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeSeenInvitesStore : SeenInvitesStore {
private val existing = MutableStateFlow(emptySet<RoomId>())
private var provided: Set<RoomId>? = null
fun publishRoomIds(invites: Set<RoomId>) {
existing.value = invites
}
fun getProvidedRoomIds() = provided
override fun seenRoomIds(): Flow<Set<RoomId>> = existing
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
provided = roomIds.toSet()
}
}

View file

@ -49,7 +49,6 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
ksp(libs.showkase.processor) ksp(libs.showkase.processor)

View file

@ -75,7 +75,6 @@ dependencies {
testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.indicator.impl)
testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.permissions.noop)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)

View file

@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
import javax.inject.Inject import javax.inject.Inject
private const val EXTENDED_RANGE_SIZE = 40 private const val EXTENDED_RANGE_SIZE = 40
@ -292,10 +293,11 @@ class RoomListPresenter @Inject constructor(
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
client.roomListService.updateAllRoomsVisibleRange(extendedRange) client.roomListService.updateAllRoomsVisibleRange(extendedRange)
} }
private fun RoomListRoomSummary.toInviteData() = InviteData(
roomId = roomId,
roomName = name,
isDirect = isDirect,
)
} }
@VisibleForTesting
internal fun RoomListRoomSummary.toInviteData() = InviteData(
roomId = roomId,
roomName = name,
isDirect = isDirect,
)

View file

@ -23,7 +23,8 @@ import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.leaveroom.api.aLeaveRoomState import io.element.android.features.leaveroom.api.aLeaveRoomState
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
import io.element.android.features.roomlist.impl.model.DisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.features.roomlist.impl.model.InviteSender
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.RoomListSearchState
@ -83,6 +84,17 @@ internal fun aRoomListState(
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> { internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
return persistentListOf( return persistentListOf(
aRoomListRoomSummary(
name = "Room Invited",
avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem),
id = "!roomId:domain",
inviteSender = InviteSender(
userId = UserId("@bob:domain"),
displayName = "Bob",
avatarData = AvatarData("@bob:domain", "Bob", size = AvatarSize.InviteSender),
),
displayType = RoomSummaryDisplayType.INVITE,
),
aRoomListRoomSummary( aRoomListRoomSummary(
name = "Room", name = "Room",
numberOfUnreadMessages = 1, numberOfUnreadMessages = 1,
@ -101,11 +113,11 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
), ),
aRoomListRoomSummary( aRoomListRoomSummary(
id = "!roomId3:domain", id = "!roomId3:domain",
displayType = DisplayType.PLACEHOLDER, displayType = RoomSummaryDisplayType.PLACEHOLDER,
), ),
aRoomListRoomSummary( aRoomListRoomSummary(
id = "!roomId4:domain", id = "!roomId4:domain",
displayType = DisplayType.PLACEHOLDER, displayType = RoomSummaryDisplayType.PLACEHOLDER,
), ),
) )
} }

View file

@ -170,7 +170,7 @@ private fun RoomListScaffold(
) )
} }
internal fun RoomListRoomSummary.contentType() = type.ordinal internal fun RoomListRoomSummary.contentType() = displayType.ordinal
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable

View file

@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.roomlist.impl.RoomListEvents import io.element.android.features.roomlist.impl.RoomListEvents
import io.element.android.features.roomlist.impl.model.DisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.features.roomlist.impl.model.InviteSender import io.element.android.features.roomlist.impl.model.InviteSender
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
@ -78,11 +78,11 @@ internal fun RoomSummaryRow(
eventSink: (RoomListEvents) -> Unit, eventSink: (RoomListEvents) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (room.type) { when (room.displayType) {
DisplayType.PLACEHOLDER -> { RoomSummaryDisplayType.PLACEHOLDER -> {
RoomSummaryPlaceholderRow(modifier = modifier) RoomSummaryPlaceholderRow(modifier = modifier)
} }
DisplayType.INVITE -> { RoomSummaryDisplayType.INVITE -> {
RoomSummaryScaffoldRow( RoomSummaryScaffoldRow(
room = room, room = room,
onClick = onClick, onClick = onClick,
@ -107,7 +107,7 @@ internal fun RoomSummaryRow(
}) })
} }
} }
DisplayType.ROOM -> { RoomSummaryDisplayType.ROOM -> {
RoomSummaryScaffoldRow( RoomSummaryScaffoldRow(
room = room, room = room,
onClick = onClick, onClick = onClick,

View file

@ -18,7 +18,7 @@ package io.element.android.features.roomlist.impl.datasource
import io.element.android.features.roomlist.impl.model.InviteSender import io.element.android.features.roomlist.impl.model.InviteSender
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.DisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty import io.element.android.libraries.core.extensions.orEmpty
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
@ -38,7 +38,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
return RoomListRoomSummary( return RoomListRoomSummary(
id = id, id = id,
roomId = RoomId(id), roomId = RoomId(id),
type = DisplayType.PLACEHOLDER, displayType = RoomSummaryDisplayType.PLACEHOLDER,
name = "Short name", name = "Short name",
timestamp = "hh:mm", timestamp = "hh:mm",
lastMessage = "Last message for placeholder", lastMessage = "Last message for placeholder",
@ -95,10 +95,10 @@ class RoomListRoomSummaryFactory @Inject constructor(
) )
}, },
canonicalAlias = roomSummary.details.canonicalAlias, canonicalAlias = roomSummary.details.canonicalAlias,
type = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) { displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) {
DisplayType.INVITE RoomSummaryDisplayType.INVITE
} else { } else {
DisplayType.ROOM RoomSummaryDisplayType.ROOM
} }
) )
} }

View file

@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@Immutable @Immutable
data class RoomListRoomSummary( data class RoomListRoomSummary(
val id: String, val id: String,
val type: DisplayType, val displayType: RoomSummaryDisplayType,
val roomId: RoomId, val roomId: RoomId,
val name: String, val name: String,
val canonicalAlias: String?, val canonicalAlias: String?,
@ -45,11 +45,11 @@ data class RoomListRoomSummary(
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
isMarkedUnread || isMarkedUnread ||
type == DisplayType.INVITE displayType == RoomSummaryDisplayType.INVITE
val hasNewContent = numberOfUnreadMessages > 0 || val hasNewContent = numberOfUnreadMessages > 0 ||
numberOfUnreadMentions > 0 || numberOfUnreadMentions > 0 ||
numberOfUnreadNotifications > 0 || numberOfUnreadNotifications > 0 ||
isMarkedUnread || isMarkedUnread ||
type == DisplayType.INVITE displayType == RoomSummaryDisplayType.INVITE
} }

View file

@ -27,7 +27,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
override val values: Sequence<RoomListRoomSummary> override val values: Sequence<RoomListRoomSummary>
get() = sequenceOf( get() = sequenceOf(
listOf( listOf(
aRoomListRoomSummary(displayType = DisplayType.PLACEHOLDER), aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
aRoomListRoomSummary(), aRoomListRoomSummary(),
aRoomListRoomSummary(lastMessage = null), aRoomListRoomSummary(lastMessage = null),
aRoomListRoomSummary( aRoomListRoomSummary(
@ -83,7 +83,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
}.flatten(), }.flatten(),
listOf( listOf(
aRoomListRoomSummary( aRoomListRoomSummary(
displayType = DisplayType.INVITE, displayType = RoomSummaryDisplayType.INVITE,
inviteSender = InviteSender( inviteSender = InviteSender(
userId = UserId("@alice:matrix.org"), userId = UserId("@alice:matrix.org"),
displayName = "Alice", displayName = "Alice",
@ -93,7 +93,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
), ),
aRoomListRoomSummary( aRoomListRoomSummary(
name = "Bob", name = "Bob",
displayType = DisplayType.INVITE, displayType = RoomSummaryDisplayType.INVITE,
inviteSender = InviteSender( inviteSender = InviteSender(
userId = UserId("@bob:matrix.org"), userId = UserId("@bob:matrix.org"),
displayName = "Bob", displayName = "Bob",
@ -120,7 +120,7 @@ internal fun aRoomListRoomSummary(
isDirect: Boolean = false, isDirect: Boolean = false,
isFavorite: Boolean = false, isFavorite: Boolean = false,
inviteSender: InviteSender? = null, inviteSender: InviteSender? = null,
displayType: DisplayType = DisplayType.ROOM, displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
canonicalAlias: String? = null, canonicalAlias: String? = null,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = id, id = id,
@ -138,6 +138,6 @@ internal fun aRoomListRoomSummary(
isDirect = isDirect, isDirect = isDirect,
isFavorite = isFavorite, isFavorite = isFavorite,
inviteSender = inviteSender, inviteSender = inviteSender,
type = displayType, displayType = displayType,
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
) )

View file

@ -19,7 +19,7 @@ package io.element.android.features.roomlist.impl.model
/** /**
* Represents the type of display for a room list item. * Represents the type of display for a room list item.
*/ */
enum class DisplayType { enum class RoomSummaryDisplayType {
PLACEHOLDER, PLACEHOLDER,
ROOM, ROOM,
INVITE INVITE

View file

@ -21,13 +21,15 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomPresenter import io.element.android.features.leaveroom.api.LeaveRoomPresenter
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
@ -51,6 +53,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.BackupState
import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
@ -76,15 +79,20 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.MutablePresenter import io.element.android.tests.testutils.MutablePresenter
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
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 java.util.Optional
class RoomListPresenterTests { class RoomListPresenterTests {
@get:Rule @get:Rule
@ -302,38 +310,6 @@ class RoomListPresenterTests {
} }
} }
@Test
fun `present - sets invite state`() = runTest {
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow)
val roomListService = FakeRoomListService()
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val presenter = createRoomListPresenter(
inviteStateDataSource = inviteStateDataSource,
coroutineScope = scope,
client = FakeMatrixClient(roomListService = roomListService),
)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val firstItem = consumeItemsUntilPredicate {
it.contentState is RoomListContentState.Rooms
}.last()
assertThat(firstItem.contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
inviteStateFlow.value = InvitesState.SeenInvites
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
inviteStateFlow.value = InvitesState.NewInvites
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NewInvites)
inviteStateFlow.value = InvitesState.NoInvites
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
scope.cancel()
}
}
@Test @Test
fun `present - show context menu`() = runTest { fun `present - show context menu`() = runTest {
val scope = CoroutineScope(coroutineContext + SupervisorJob()) val scope = CoroutineScope(coroutineContext + SupervisorJob())
@ -608,11 +584,53 @@ class RoomListPresenterTests {
} }
} }
@Test
fun `present - when a room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
val roomListService = FakeRoomListService()
val scope = CoroutineScope(coroutineContext + SupervisorJob())
val matrixClient = FakeMatrixClient(
roomListService = roomListService,
)
val roomSummary = aRoomSummaryFilled(
currentUserMembership = CurrentUserMembership.INVITED
)
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(roomSummary))
val presenter = createRoomListPresenter(
coroutineScope = scope,
client = matrixClient,
acceptDeclineInvitePresenter = acceptDeclinePresenter
)
presenter.test {
val state = consumeItemsUntilPredicate {
it.contentState is RoomListContentState.Rooms
}.last()
val roomListRoomSummary = state.contentAsRooms().summaries.first {
it.id == roomSummary.identifier()
}
state.eventSink(RoomListEvents.AcceptInvite(roomListRoomSummary))
state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary))
val inviteData = roomListRoomSummary.toInviteData()
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))),
)
}
}
private fun TestScope.createRoomListPresenter( private fun TestScope.createRoomListPresenter(
client: MatrixClient = FakeMatrixClient(), client: MatrixClient = FakeMatrixClient(),
networkMonitor: NetworkMonitor = FakeNetworkMonitor(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(),
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
givenFormat(A_FORMATTED_DATE) givenFormat(A_FORMATTED_DATE)
@ -625,11 +643,11 @@ class RoomListPresenterTests {
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() }, filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() }, searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
) = RoomListPresenter( ) = RoomListPresenter(
client = client, client = client,
networkMonitor = networkMonitor, networkMonitor = networkMonitor,
snackbarDispatcher = snackbarDispatcher, snackbarDispatcher = snackbarDispatcher,
inviteStateDataSource = inviteStateDataSource,
leaveRoomPresenter = leaveRoomPresenter, leaveRoomPresenter = leaveRoomPresenter,
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomListService = client.roomListService, roomListService = client.roomListService,
@ -651,5 +669,6 @@ class RoomListPresenterTests {
sessionPreferencesStore = sessionPreferencesStore, sessionPreferencesStore = sessionPreferencesStore,
filtersPresenter = filtersPresenter, filtersPresenter = filtersPresenter,
analyticsService = analyticsService, analyticsService = analyticsService,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
) )
} }

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.roomlist.impl.components.RoomListMenuAction import io.element.android.features.roomlist.impl.components.RoomListMenuAction
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -93,7 +94,9 @@ class RoomListViewTest {
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val room0 = state.contentAsRooms().summaries.first() val room0 = state.contentAsRooms().summaries.first{
it.displayType == RoomSummaryDisplayType.ROOM
}
ensureCalledOnceWithParam(room0.roomId) { callback -> ensureCalledOnceWithParam(room0.roomId) { callback ->
rule.setRoomListView( rule.setRoomListView(
state = state, state = state,
@ -109,7 +112,9 @@ class RoomListViewTest {
val state = aRoomListState( val state = aRoomListState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
val room0 = state.contentAsRooms().summaries.first() val room0 = state.contentAsRooms().summaries.first{
it.displayType == RoomSummaryDisplayType.ROOM
}
rule.setRoomListView( rule.setRoomListView(
state = state, state = state,
) )
@ -136,19 +141,20 @@ class RoomListViewTest {
} }
@Test @Test
fun `clicking on invites invokes the expected callback`() { fun `clicking on accept and decline invite emits the expected Events`() {
val eventsRecorder = EventsRecorder<RoomListEvents>() val eventsRecorder = EventsRecorder<RoomListEvents>()
val state = aRoomListState( val state = aRoomListState(
contentState = aRoomsContentState(invitesState = InvitesState.NewInvites),
eventSink = eventsRecorder, eventSink = eventsRecorder,
) )
ensureCalledOnce { callback -> val invitedRoom = state.contentAsRooms().summaries.first {
rule.setRoomListView( it.displayType == RoomSummaryDisplayType.INVITE
state = state,
onInvitesClicked = callback,
)
rule.clickOn(CommonStrings.action_invites_list)
} }
rule.setRoomListView(state = state)
rule.clickOn(CommonStrings.action_accept)
rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertList(
listOf(RoomListEvents.AcceptInvite(invitedRoom),RoomListEvents.DeclineInvite(invitedRoom)),
)
} }
} }
@ -158,7 +164,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onSettingsClicked: () -> Unit = EnsureNeverCalled(), onSettingsClicked: () -> Unit = EnsureNeverCalled(),
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(), onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(), onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(), onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
@ -170,10 +175,10 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
onSettingsClicked = onSettingsClicked, onSettingsClicked = onSettingsClicked,
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
onCreateRoomClicked = onCreateRoomClicked, onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
onRoomSettingsClicked = onRoomSettingsClicked, onRoomSettingsClicked = onRoomSettingsClicked,
onMenuActionClicked = onMenuActionClicked, onMenuActionClicked = onMenuActionClicked,
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
acceptDeclineInviteView = { },
) )
} }
} }

View file

@ -1,134 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.datasource
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.test.FakeSeenInvitesStore
import io.element.android.features.roomlist.impl.InvitesState
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NoInvites state if invites list is empty`() = runTest {
val roomListService = FakeRoomListService()
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
moleculeFlow(RecompositionMode.Immediate) {
dataSource.inviteState()
}.test {
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
}
}
@Test
fun `emits NewInvites state if unseen invite exists`() = runTest {
val roomListService = FakeRoomListService()
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
moleculeFlow(RecompositionMode.Immediate) {
dataSource.inviteState()
}.test {
skipItems(2)
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
}
}
@Test
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
val roomListService = FakeRoomListService()
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
moleculeFlow(RecompositionMode.Immediate) {
dataSource.inviteState()
}.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
}
}
@Test
fun `emits SeenInvites state if invite exists in seen store`() = runTest {
val roomListService = FakeRoomListService()
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
moleculeFlow(RecompositionMode.Immediate) {
dataSource.inviteState()
}.test {
skipItems(1)
assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
}
}
@Test
fun `emits new state in response to upstream events`() = runTest {
val roomListService = FakeRoomListService()
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
moleculeFlow(RecompositionMode.Immediate) {
dataSource.inviteState()
}.test {
// Initially there are no invites
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
// When a single invite is received, state should be NewInvites
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
skipItems(1)
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
// If that invite is marked as seen, then the state becomes SeenInvites
seenStore.publishRoomIds(setOf(A_ROOM_ID))
skipItems(1)
assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
// Another new invite resets it to NewInvites
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
skipItems(1)
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
// All of the invites going away reverts to NoInvites
roomListService.postInviteRooms(emptyList())
skipItems(1)
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
}
}
}

View file

@ -1,33 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.roomlist.impl.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import io.element.android.features.roomlist.impl.InvitesState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class FakeInviteDataSource(
private val flow: Flow<InvitesState> = flowOf()
) : InviteStateDataSource {
@Composable
override fun inviteState(): InvitesState {
val state = flow.collectAsState(initial = InvitesState.NoInvites)
return state.value
}
}

View file

@ -45,6 +45,7 @@ class RoomListFiltersPresenterTests {
filterSelectionState(RoomListFilter.People, false), filterSelectionState(RoomListFilter.People, false),
filterSelectionState(RoomListFilter.Rooms, false), filterSelectionState(RoomListFilter.Rooms, false),
filterSelectionState(RoomListFilter.Favourites, false), filterSelectionState(RoomListFilter.Favourites, false),
filterSelectionState(RoomListFilter.Invites, false),
) )
} }
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -84,6 +85,7 @@ class RoomListFiltersPresenterTests {
filterSelectionState(RoomListFilter.People, false), filterSelectionState(RoomListFilter.People, false),
filterSelectionState(RoomListFilter.Rooms, false), filterSelectionState(RoomListFilter.Rooms, false),
filterSelectionState(RoomListFilter.Favourites, false), filterSelectionState(RoomListFilter.Favourites, false),
filterSelectionState(RoomListFilter.Invites, false),
).inOrder() ).inOrder()
assertThat(state.selectedFilters()).isEmpty() assertThat(state.selectedFilters()).isEmpty()
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All

View file

@ -72,6 +72,15 @@ class RoomListRoomSummaryTest {
assertThat(sut.isHighlighted).isTrue() assertThat(sut.isHighlighted).isTrue()
assertThat(sut.hasNewContent).isTrue() assertThat(sut.hasNewContent).isTrue()
} }
@Test
fun `when display type is invite then isHighlighted and hasNewContent are true`() {
val sut = createRoomListRoomSummary(
displayType = RoomSummaryDisplayType.INVITE,
)
assertThat(sut.isHighlighted).isTrue()
assertThat(sut.hasNewContent).isTrue()
}
} }
internal fun createRoomListRoomSummary( internal fun createRoomListRoomSummary(
@ -81,6 +90,7 @@ internal fun createRoomListRoomSummary(
isMarkedUnread: Boolean = false, isMarkedUnread: Boolean = false,
userDefinedNotificationMode: RoomNotificationMode? = null, userDefinedNotificationMode: RoomNotificationMode? = null,
isFavorite: Boolean = false, isFavorite: Boolean = false,
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = A_ROOM_ID.value, id = A_ROOM_ID.value,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -92,9 +102,11 @@ internal fun createRoomListRoomSummary(
timestamp = A_FORMATTED_DATE, timestamp = A_FORMATTED_DATE,
lastMessage = "", lastMessage = "",
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
isPlaceholder = false, displayType = displayType,
userDefinedNotificationMode = userDefinedNotificationMode, userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = false, hasRoomCall = false,
isDirect = false, isDirect = false,
isFavorite = isFavorite, isFavorite = isFavorite,
canonicalAlias = null,
inviteSender = null,
) )

View file

@ -41,6 +41,7 @@ fun aRoomSummaryFilled(
numUnreadMentions: Int = 0, numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0, numUnreadMessages: Int = 0,
notificationMode: RoomNotificationMode? = null, notificationMode: RoomNotificationMode? = null,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
) = RoomSummary.Filled( ) = RoomSummary.Filled(
aRoomSummaryDetails( aRoomSummaryDetails(
roomId = roomId, roomId = roomId,
@ -51,6 +52,7 @@ fun aRoomSummaryFilled(
numUnreadMentions = numUnreadMentions, numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages, numUnreadMessages = numUnreadMessages,
notificationMode = notificationMode, notificationMode = notificationMode,
currentUserMembership = currentUserMembership,
) )
) )

View file

@ -24,6 +24,4 @@ import io.element.android.libraries.push.impl.intent.IntentProvider
class FakeIntentProvider : IntentProvider { class FakeIntentProvider : IntentProvider {
override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent() override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent()
override fun getInviteListIntent(sessionId: SessionId) = Intent()
} }

View file

@ -65,4 +65,5 @@ dependencies {
implementation(projects.libraries.featureflag.impl) implementation(projects.libraries.featureflag.impl)
implementation(projects.services.analytics.noop) implementation(projects.services.analytics.noop)
implementation(libs.coroutines.core) implementation(libs.coroutines.core)
implementation(projects.libraries.push.test)
} }

View file

@ -20,6 +20,8 @@ import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
import io.element.android.features.roomlist.impl.RoomListPresenter import io.element.android.features.roomlist.impl.RoomListPresenter
@ -48,6 +50,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -94,7 +97,6 @@ class RoomListScreen(
client = matrixClient, client = matrixClient,
networkMonitor = NetworkMonitorImpl(context, Singleton.appScope), networkMonitor = NetworkMonitorImpl(context, Singleton.appScope),
snackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher = SnackbarDispatcher(),
inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers),
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers), leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomListService = matrixClient.roomListService, roomListService = matrixClient.roomListService,
@ -130,6 +132,11 @@ class RoomListScreen(
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
filterSelectionStrategy = DefaultFilterSelectionStrategy(), filterSelectionStrategy = DefaultFilterSelectionStrategy(),
), ),
acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter(
client = matrixClient,
analyticsService = NoopAnalyticsService(),
notificationDrawerManager = FakeNotificationDrawerManager(),
),
analyticsService = NoopAnalyticsService(), analyticsService = NoopAnalyticsService(),
) )
@ -152,11 +159,13 @@ class RoomListScreen(
onSettingsClicked = {}, onSettingsClicked = {},
onConfirmRecoveryKeyClicked = {}, onConfirmRecoveryKeyClicked = {},
onCreateRoomClicked = {}, onCreateRoomClicked = {},
onInvitesClicked = {},
onRoomSettingsClicked = {}, onRoomSettingsClicked = {},
onMenuActionClicked = {}, onMenuActionClicked = {},
onRoomDirectorySearchClicked = {}, onRoomDirectorySearchClicked = {},
modifier = modifier, modifier = modifier,
acceptDeclineInviteView = {
AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onInviteAccepted = {}, onInviteDeclined = {})
}
) )
DisposableEffect(Unit) { DisposableEffect(Unit) {