diff --git a/features/invite/impl/build.gradle.kts b/features/invite/impl/build.gradle.kts index 7a0b0db372..87e21ac562 100644 --- a/features/invite/impl/build.gradle.kts +++ b/features/invite/impl/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) - testImplementation(projects.features.invite.test) testImplementation(projects.services.analytics.test) testImplementation(projects.tests.testutils) diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt deleted file mode 100644 index ae982e0358..0000000000 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/invitelist/InviteListPresenterTests.kt +++ /dev/null @@ -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.awaitInitialItem(): InviteListState { - skipItems(1) - return awaitItem() - } - - private fun createInviteListPresenter( - client: MatrixClient, - seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(), - acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, - ) = InviteListPresenter( - client, - seenInvitesStore, - acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, - ) -} diff --git a/features/invite/test/build.gradle.kts b/features/invite/test/build.gradle.kts deleted file mode 100644 index 44d9d3030c..0000000000 --- a/features/invite/test/build.gradle.kts +++ /dev/null @@ -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) -} diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/FakeSeenInvitesStore.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/FakeSeenInvitesStore.kt deleted file mode 100644 index c148eca159..0000000000 --- a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/FakeSeenInvitesStore.kt +++ /dev/null @@ -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()) - private var provided: Set? = null - - fun publishRoomIds(invites: Set) { - existing.value = invites - } - - fun getProvidedRoomIds() = provided - - override fun seenRoomIds(): Flow> = existing - - override suspend fun markAsSeen(roomIds: Set) { - provided = roomIds.toSet() - } -} diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index cfdddcfee8..03d3051546 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -49,7 +49,6 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) - testImplementation(projects.features.invite.test) testImplementation(projects.tests.testutils) ksp(libs.showkase.processor) diff --git a/features/roomlist/impl/build.gradle.kts b/features/roomlist/impl/build.gradle.kts index ff67ebe42a..e4eaf9b20e 100644 --- a/features/roomlist/impl/build.gradle.kts +++ b/features/roomlist/impl/build.gradle.kts @@ -75,7 +75,6 @@ dependencies { testImplementation(projects.libraries.indicator.impl) testImplementation(projects.libraries.permissions.noop) testImplementation(projects.libraries.preferences.test) - testImplementation(projects.features.invite.test) testImplementation(projects.services.analytics.test) testImplementation(projects.features.networkmonitor.test) testImplementation(projects.tests.testutils) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index d58398a831..edd237f56b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.launch +import org.jetbrains.annotations.VisibleForTesting import javax.inject.Inject private const val EXTENDED_RANGE_SIZE = 40 @@ -292,10 +293,11 @@ class RoomListPresenter @Inject constructor( val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) 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, +) diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt index 745d14164a..3b53723565 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListStateProvider.kt @@ -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.roomlist.impl.filters.RoomListFiltersState 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.aRoomListRoomSummary import io.element.android.features.roomlist.impl.search.RoomListSearchState @@ -83,6 +84,17 @@ internal fun aRoomListState( internal fun aRoomListRoomSummaryList(): ImmutableList { 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( name = "Room", numberOfUnreadMessages = 1, @@ -101,11 +113,11 @@ internal fun aRoomListRoomSummaryList(): ImmutableList { ), aRoomListRoomSummary( id = "!roomId3:domain", - displayType = DisplayType.PLACEHOLDER, + displayType = RoomSummaryDisplayType.PLACEHOLDER, ), aRoomListRoomSummary( id = "!roomId4:domain", - displayType = DisplayType.PLACEHOLDER, + displayType = RoomSummaryDisplayType.PLACEHOLDER, ), ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt index d4652cb975..fe874a037b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListView.kt @@ -170,7 +170,7 @@ private fun RoomListScaffold( ) } -internal fun RoomListRoomSummary.contentType() = type.ordinal +internal fun RoomListRoomSummary.contentType() = displayType.ordinal @PreviewsDayNight @Composable diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt index ecc9b0bd83..0d30d93f47 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/components/RoomSummaryRow.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons 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.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider @@ -78,11 +78,11 @@ internal fun RoomSummaryRow( eventSink: (RoomListEvents) -> Unit, modifier: Modifier = Modifier, ) { - when (room.type) { - DisplayType.PLACEHOLDER -> { + when (room.displayType) { + RoomSummaryDisplayType.PLACEHOLDER -> { RoomSummaryPlaceholderRow(modifier = modifier) } - DisplayType.INVITE -> { + RoomSummaryDisplayType.INVITE -> { RoomSummaryScaffoldRow( room = room, onClick = onClick, @@ -107,7 +107,7 @@ internal fun RoomSummaryRow( }) } } - DisplayType.ROOM -> { + RoomSummaryDisplayType.ROOM -> { RoomSummaryScaffoldRow( room = room, onClick = onClick, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt index a92c56e94f..af631c3181 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt @@ -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.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.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -38,7 +38,7 @@ class RoomListRoomSummaryFactory @Inject constructor( return RoomListRoomSummary( id = id, roomId = RoomId(id), - type = DisplayType.PLACEHOLDER, + displayType = RoomSummaryDisplayType.PLACEHOLDER, name = "Short name", timestamp = "hh:mm", lastMessage = "Last message for placeholder", @@ -95,10 +95,10 @@ class RoomListRoomSummaryFactory @Inject constructor( ) }, canonicalAlias = roomSummary.details.canonicalAlias, - type = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) { - DisplayType.INVITE + displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) { + RoomSummaryDisplayType.INVITE } else { - DisplayType.ROOM + RoomSummaryDisplayType.ROOM } ) } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt index 996fc18c66..c149bc8b2b 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummary.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode @Immutable data class RoomListRoomSummary( val id: String, - val type: DisplayType, + val displayType: RoomSummaryDisplayType, val roomId: RoomId, val name: String, val canonicalAlias: String?, @@ -45,11 +45,11 @@ data class RoomListRoomSummary( val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE && (numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) || isMarkedUnread || - type == DisplayType.INVITE + displayType == RoomSummaryDisplayType.INVITE val hasNewContent = numberOfUnreadMessages > 0 || numberOfUnreadMentions > 0 || numberOfUnreadNotifications > 0 || isMarkedUnread || - type == DisplayType.INVITE + displayType == RoomSummaryDisplayType.INVITE } diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt index d33803fd2b..68abb742dd 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryProvider.kt @@ -27,7 +27,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider get() = sequenceOf( listOf( - aRoomListRoomSummary(displayType = DisplayType.PLACEHOLDER), + aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER), aRoomListRoomSummary(), aRoomListRoomSummary(lastMessage = null), aRoomListRoomSummary( @@ -83,7 +83,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider } + 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( client: MatrixClient = FakeMatrixClient(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), - inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(), leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(), lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { givenFormat(A_FORMATTED_DATE) @@ -625,11 +643,11 @@ class RoomListPresenterTests { analyticsService: AnalyticsService = FakeAnalyticsService(), filtersPresenter: Presenter = Presenter { aRoomListFiltersState() }, searchPresenter: Presenter = Presenter { aRoomListSearchState() }, + acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() }, ) = RoomListPresenter( client = client, networkMonitor = networkMonitor, snackbarDispatcher = snackbarDispatcher, - inviteStateDataSource = inviteStateDataSource, leaveRoomPresenter = leaveRoomPresenter, roomListDataSource = RoomListDataSource( roomListService = client.roomListService, @@ -651,5 +669,6 @@ class RoomListPresenterTests { sessionPreferencesStore = sessionPreferencesStore, filtersPresenter = filtersPresenter, analyticsService = analyticsService, + acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, ) } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt index 2dbec36395..c21a3a9410 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListViewTest.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.test.ext.junit.runners.AndroidJUnit4 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.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled @@ -93,7 +94,9 @@ class RoomListViewTest { val state = aRoomListState( eventSink = eventsRecorder, ) - val room0 = state.contentAsRooms().summaries.first() + val room0 = state.contentAsRooms().summaries.first{ + it.displayType == RoomSummaryDisplayType.ROOM + } ensureCalledOnceWithParam(room0.roomId) { callback -> rule.setRoomListView( state = state, @@ -109,7 +112,9 @@ class RoomListViewTest { val state = aRoomListState( eventSink = eventsRecorder, ) - val room0 = state.contentAsRooms().summaries.first() + val room0 = state.contentAsRooms().summaries.first{ + it.displayType == RoomSummaryDisplayType.ROOM + } rule.setRoomListView( state = state, ) @@ -136,19 +141,20 @@ class RoomListViewTest { } @Test - fun `clicking on invites invokes the expected callback`() { + fun `clicking on accept and decline invite emits the expected Events`() { val eventsRecorder = EventsRecorder() val state = aRoomListState( - contentState = aRoomsContentState(invitesState = InvitesState.NewInvites), eventSink = eventsRecorder, ) - ensureCalledOnce { callback -> - rule.setRoomListView( - state = state, - onInvitesClicked = callback, - ) - rule.clickOn(CommonStrings.action_invites_list) + val invitedRoom = state.contentAsRooms().summaries.first { + it.displayType == RoomSummaryDisplayType.INVITE } + 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 AndroidComposeTestRule.setRoomL onSettingsClicked: () -> Unit = EnsureNeverCalled(), onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(), onCreateRoomClicked: () -> Unit = EnsureNeverCalled(), - onInvitesClicked: () -> Unit = EnsureNeverCalled(), onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(), onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(), onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(), @@ -170,10 +175,10 @@ private fun AndroidComposeTestRule.setRoomL onSettingsClicked = onSettingsClicked, onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked, onCreateRoomClicked = onCreateRoomClicked, - onInvitesClicked = onInvitesClicked, onRoomSettingsClicked = onRoomSettingsClicked, onMenuActionClicked = onMenuActionClicked, onRoomDirectorySearchClicked = onRoomDirectorySearchClicked, + acceptDeclineInviteView = { }, ) } } diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt deleted file mode 100644 index a1e08cd93b..0000000000 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/DefaultInviteStateDataSourceTest.kt +++ /dev/null @@ -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) - } - } -} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/FakeInviteDataSource.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/FakeInviteDataSource.kt deleted file mode 100644 index 49f4a65f7f..0000000000 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/FakeInviteDataSource.kt +++ /dev/null @@ -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 = flowOf() -) : InviteStateDataSource { - @Composable - override fun inviteState(): InvitesState { - val state = flow.collectAsState(initial = InvitesState.NoInvites) - return state.value - } -} diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt index 756fe1aa0e..0a81f546c5 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/filters/RoomListFiltersPresenterTests.kt @@ -45,6 +45,7 @@ class RoomListFiltersPresenterTests { filterSelectionState(RoomListFilter.People, false), filterSelectionState(RoomListFilter.Rooms, false), filterSelectionState(RoomListFilter.Favourites, false), + filterSelectionState(RoomListFilter.Invites, false), ) } cancelAndIgnoreRemainingEvents() @@ -84,6 +85,7 @@ class RoomListFiltersPresenterTests { filterSelectionState(RoomListFilter.People, false), filterSelectionState(RoomListFilter.Rooms, false), filterSelectionState(RoomListFilter.Favourites, false), + filterSelectionState(RoomListFilter.Invites, false), ).inOrder() assertThat(state.selectedFilters()).isEmpty() val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt index 82816e674f..db0bdf9ded 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt @@ -72,6 +72,15 @@ class RoomListRoomSummaryTest { assertThat(sut.isHighlighted).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( @@ -81,6 +90,7 @@ internal fun createRoomListRoomSummary( isMarkedUnread: Boolean = false, userDefinedNotificationMode: RoomNotificationMode? = null, isFavorite: Boolean = false, + displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM, ) = RoomListRoomSummary( id = A_ROOM_ID.value, roomId = A_ROOM_ID, @@ -92,9 +102,11 @@ internal fun createRoomListRoomSummary( timestamp = A_FORMATTED_DATE, lastMessage = "", avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), - isPlaceholder = false, + displayType = displayType, userDefinedNotificationMode = userDefinedNotificationMode, hasRoomCall = false, isDirect = false, isFavorite = isFavorite, + canonicalAlias = null, + inviteSender = null, ) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index badb68e9e1..b9b078c5f3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -41,6 +41,7 @@ fun aRoomSummaryFilled( numUnreadMentions: Int = 0, numUnreadMessages: Int = 0, notificationMode: RoomNotificationMode? = null, + currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, ) = RoomSummary.Filled( aRoomSummaryDetails( roomId = roomId, @@ -51,6 +52,7 @@ fun aRoomSummaryFilled( numUnreadMentions = numUnreadMentions, numUnreadMessages = numUnreadMessages, notificationMode = notificationMode, + currentUserMembership = currentUserMembership, ) ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt index 2de70bb672..ed64e98856 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/FakeIntentProvider.kt @@ -24,6 +24,4 @@ import io.element.android.libraries.push.impl.intent.IntentProvider class FakeIntentProvider : IntentProvider { override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent() - - override fun getInviteListIntent(sessionId: SessionId) = Intent() } diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 402d1a48b8..80f5f8726b 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -65,4 +65,5 @@ dependencies { implementation(projects.libraries.featureflag.impl) implementation(projects.services.analytics.noop) implementation(libs.coroutines.core) + implementation(projects.libraries.push.test) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index a7916a13da..49fa29ced3 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -20,6 +20,8 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect 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.networkmonitor.impl.NetworkMonitorImpl 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.room.RoomMembershipObserver 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.toolbox.impl.strings.AndroidStringProvider import kotlinx.coroutines.launch @@ -94,7 +97,6 @@ class RoomListScreen( client = matrixClient, networkMonitor = NetworkMonitorImpl(context, Singleton.appScope), snackbarDispatcher = SnackbarDispatcher(), - inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers), roomListDataSource = RoomListDataSource( roomListService = matrixClient.roomListService, @@ -130,6 +132,11 @@ class RoomListScreen( featureFlagService = featureFlagService, filterSelectionStrategy = DefaultFilterSelectionStrategy(), ), + acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter( + client = matrixClient, + analyticsService = NoopAnalyticsService(), + notificationDrawerManager = FakeNotificationDrawerManager(), + ), analyticsService = NoopAnalyticsService(), ) @@ -152,11 +159,13 @@ class RoomListScreen( onSettingsClicked = {}, onConfirmRecoveryKeyClicked = {}, onCreateRoomClicked = {}, - onInvitesClicked = {}, onRoomSettingsClicked = {}, onMenuActionClicked = {}, onRoomDirectorySearchClicked = {}, modifier = modifier, + acceptDeclineInviteView = { + AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onInviteAccepted = {}, onInviteDeclined = {}) + } ) DisposableEffect(Unit) {