Merge pull request #8159 from vector-im/feature/mna/aggregated-unread-indicator

Add aggregated unread indicator for spaces in the new layout
This commit is contained in:
Maxime NATUREL 2023-02-24 09:56:54 +01:00 committed by GitHub
commit a6f7302350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 790 additions and 39 deletions

1
changelog.d/8157.feature Normal file
View File

@ -0,0 +1 @@
Add aggregated unread indicator for spaces in the new layout

View File

@ -40,6 +40,7 @@ import im.vector.app.features.discovery.DiscoverySettingsViewModel
import im.vector.app.features.discovery.change.SetIdentityServerViewModel
import im.vector.app.features.home.HomeActivityViewModel
import im.vector.app.features.home.HomeDetailViewModel
import im.vector.app.features.home.NewHomeDetailViewModel
import im.vector.app.features.home.UnknownDeviceDetectorSharedViewModel
import im.vector.app.features.home.UnreadMessagesSharedViewModel
import im.vector.app.features.home.UserColorAccountDataViewModel
@ -717,4 +718,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(RoomPollDetailViewModel::class)
fun roomPollDetailViewModelFactory(factory: RoomPollDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(NewHomeDetailViewModel::class)
fun newHomeDetailViewModelFactory(factory: NewHomeDetailViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View File

@ -0,0 +1,67 @@
/*
* 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 im.vector.app.features.home
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.spaces.GetSpacesUseCase
import im.vector.app.features.spaces.notification.GetNotificationCountForSpacesUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import javax.inject.Inject
class GetSpacesNotificationBadgeStateUseCase @Inject constructor(
private val getNotificationCountForSpacesUseCase: GetNotificationCountForSpacesUseCase,
private val getSpacesUseCase: GetSpacesUseCase,
) {
fun execute(): Flow<UnreadCounterBadgeView.State> {
val params = spaceSummaryQueryParams {
memberships = listOf(Membership.INVITE)
displayName = QueryStringValue.IsNotEmpty
}
return combine(
getNotificationCountForSpacesUseCase.execute(SpaceFilter.NoFilter),
getSpacesUseCase.execute(params),
) { spacesNotificationCount, spaceInvites ->
computeSpacesNotificationCounterBadgeState(spacesNotificationCount, spaceInvites)
}
}
private fun computeSpacesNotificationCounterBadgeState(
spacesNotificationCount: RoomAggregateNotificationCount,
spaceInvites: List<RoomSummary>,
): UnreadCounterBadgeView.State {
val hasPendingSpaceInvites = spaceInvites.isNotEmpty()
return if (hasPendingSpaceInvites && spacesNotificationCount.notificationCount == 0) {
UnreadCounterBadgeView.State.Text(
text = "!",
highlighted = true,
)
} else {
UnreadCounterBadgeView.State.Count(
count = spacesNotificationCount.notificationCount,
highlighted = spacesNotificationCount.isHighlight || hasPendingSpaceInvites,
)
}
}
}

View File

@ -47,6 +47,7 @@ import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.PstnDialActivity
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.home.room.list.actions.RoomListSharedAction
import im.vector.app.features.home.room.list.actions.RoomListSharedActionViewModel
import im.vector.app.features.home.room.list.home.HomeRoomListFragment
@ -82,6 +83,7 @@ class NewHomeDetailFragment :
@Inject lateinit var buildMeta: BuildMeta
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private val newHomeDetailViewModel: NewHomeDetailViewModel by fragmentViewModel()
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
@ -180,6 +182,10 @@ class NewHomeDetailFragment :
currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
invalidateOptionsMenu()
}
newHomeDetailViewModel.onEach { viewState ->
refreshUnreadCounterBadge(viewState.spacesNotificationCounterBadgeState)
}
}
private fun setupObservers() {
@ -379,6 +385,10 @@ class NewHomeDetailFragment :
}
}
private fun refreshUnreadCounterBadge(badgeState: UnreadCounterBadgeView.State) {
views.spacesUnreadCounterBadge.render(badgeState)
}
override fun onTapToReturnToCall() {
callManager.getCurrentCall()?.let { call ->
VectorCallActivity.newIntent(

View File

@ -0,0 +1,56 @@
/*
* 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 im.vector.app.features.home
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class NewHomeDetailViewModel @AssistedInject constructor(
@Assisted initialState: NewHomeDetailViewState,
private val getSpacesNotificationBadgeStateUseCase: GetSpacesNotificationBadgeStateUseCase,
) : VectorViewModel<NewHomeDetailViewState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<NewHomeDetailViewModel, NewHomeDetailViewState> {
override fun create(initialState: NewHomeDetailViewState): NewHomeDetailViewModel
}
companion object : MavericksViewModelFactory<NewHomeDetailViewModel, NewHomeDetailViewState> by hiltMavericksViewModelFactory()
init {
observeSpacesNotificationBadgeState()
}
private fun observeSpacesNotificationBadgeState() {
getSpacesNotificationBadgeStateUseCase.execute()
.onEach { badgeState -> setState { copy(spacesNotificationCounterBadgeState = badgeState) } }
.launchIn(viewModelScope)
}
override fun handle(action: EmptyAction) {
// do nothing
}
}

View File

@ -0,0 +1,24 @@
/*
* 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 im.vector.app.features.home
import com.airbnb.mvrx.MavericksState
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
data class NewHomeDetailViewState(
val spacesNotificationCounterBadgeState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State.Count(count = 0, highlighted = false),
) : MavericksState

View File

@ -0,0 +1,36 @@
/*
* 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 im.vector.app.features.spaces
import im.vector.app.core.di.ActiveSessionHolder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.flow.flow
import javax.inject.Inject
class GetSpacesUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
fun execute(params: SpaceSummaryQueryParams): Flow<List<RoomSummary>> {
val session = activeSessionHolder.getSafeActiveSession()
return session?.flow()?.liveSpaceSummaries(params) ?: emptyFlow()
}
}

View File

@ -29,16 +29,13 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
import im.vector.app.features.spaces.notification.GetNotificationCountForSpacesUseCase
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue
@ -48,25 +45,22 @@ import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.session.space.SpaceOrderUtils
import org.matrix.android.sdk.api.session.space.model.SpaceOrderContent
import org.matrix.android.sdk.api.session.space.model.TopLevelSpaceComparator
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.flow.flow
class SpaceListViewModel @AssistedInject constructor(
@Assisted initialState: SpaceListViewState,
private val spaceStateHandler: SpaceStateHandler,
private val session: Session,
private val vectorPreferences: VectorPreferences,
private val autoAcceptInvites: AutoAcceptInvites,
private val analyticsTracker: AnalyticsTracker,
getNotificationCountForSpacesUseCase: GetNotificationCountForSpacesUseCase,
private val getSpacesUseCase: GetSpacesUseCase,
) : VectorViewModel<SpaceListViewState, SpaceListAction, SpaceListViewEvents>(initialState) {
@AssistedFactory
@ -92,39 +86,14 @@ class SpaceListViewModel @AssistedInject constructor(
copy(selectedSpace = selectedSpaceOption.orNull())
}
// XXX there should be a way to refactor this and share it
session.roomService().getPagedRoomSummariesLive(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.spaceFilter = roomsInSpaceFilter()
}, sortOrder = RoomSortOrder.NONE
).asFlow()
.sample(300)
.onEach {
val inviteCount = if (autoAcceptInvites.hideInvites) {
0
} else {
session.roomService().getRoomSummaries(
roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) }
).size
}
val totalCount = session.roomService().getNotificationCountForRooms(
roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.spaceFilter = roomsInSpaceFilter()
}
)
val counts = RoomAggregateNotificationCount(
totalCount.notificationCount + inviteCount,
totalCount.highlightCount + inviteCount
)
getNotificationCountForSpacesUseCase.execute(roomsInSpaceFilter())
.onEach { counts ->
setState {
copy(
homeAggregateCount = counts
)
}
}
.flowOn(Dispatchers.Default)
.launchIn(viewModelScope)
}
@ -267,7 +236,7 @@ class SpaceListViewModel @AssistedInject constructor(
}
combine(
session.flow().liveSpaceSummaries(params),
getSpacesUseCase.execute(params),
session.accountDataService()
.getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER))
.asFlow()

View File

@ -0,0 +1,68 @@
/*
* 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 im.vector.app.features.spaces.notification
import androidx.lifecycle.asFlow
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.invite.AutoAcceptInvites
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.sample
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import javax.inject.Inject
class GetNotificationCountForSpacesUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
private val autoAcceptInvites: AutoAcceptInvites,
) {
fun execute(spaceFilter: SpaceFilter): Flow<RoomAggregateNotificationCount> {
val session = activeSessionHolder.getSafeActiveSession()
val spaceQueryParams = roomSummaryQueryParams {
this.memberships = listOf(Membership.JOIN)
this.spaceFilter = spaceFilter
}
return session
?.roomService()
?.getPagedRoomSummariesLive(queryParams = spaceQueryParams, sortOrder = RoomSortOrder.NONE)
?.asFlow()
?.sample(300)
?.mapLatest {
val inviteCount = if (autoAcceptInvites.hideInvites) {
0
} else {
session.roomService().getRoomSummaries(
roomSummaryQueryParams { this.memberships = listOf(Membership.INVITE) }
).size
}
val totalCount = session.roomService().getNotificationCountForRooms(spaceQueryParams)
RoomAggregateNotificationCount(
notificationCount = totalCount.notificationCount + inviteCount,
highlightCount = totalCount.highlightCount + inviteCount,
)
}
?.flowOn(session.coroutineDispatchers.main)
?: emptyFlow()
}
}

View File

@ -120,6 +120,28 @@
tools:targetApi="lollipop_mr1"
tools:visibility="visible" />
<im.vector.app.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/spacesUnreadCounterBadge"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:elevation="15dp"
android:gravity="center"
android:minWidth="16dp"
android:minHeight="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textColor="?colorOnError"
app:layout_constraintCircle="@id/newLayoutOpenSpacesButton"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="24dp"
tools:background="@drawable/bg_unread_highlight"
tools:ignore="MissingConstraints"
tools:text="147"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/newLayoutCreateChatButton"
style="@style/Widget.Vector.FloatingActionButton"

View File

@ -0,0 +1,88 @@
/*
* 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 im.vector.app.features.home
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.spaces.GetSpacesUseCase
import im.vector.app.features.spaces.notification.GetNotificationCountForSpacesUseCase
import im.vector.app.test.test
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
internal class GetSpacesNotificationBadgeStateUseCaseTest {
private val fakeGetNotificationCountForSpacesUseCase = mockk<GetNotificationCountForSpacesUseCase>()
private val fakeGetSpacesUseCase = mockk<GetSpacesUseCase>()
private val getSpacesNotificationBadgeStateUseCase = GetSpacesNotificationBadgeStateUseCase(
getNotificationCountForSpacesUseCase = fakeGetNotificationCountForSpacesUseCase,
getSpacesUseCase = fakeGetSpacesUseCase,
)
@Test
fun `given flow of spaces invite and notification count then flow of state is correct`() = runTest {
// Given
val noSpacesInvite = emptyList<RoomSummary>()
val existingSpaceInvite = listOf<RoomSummary>(mockk())
val noNotification = RoomAggregateNotificationCount(
notificationCount = 0,
highlightCount = 0,
)
val existingNotificationNotHighlighted = RoomAggregateNotificationCount(
notificationCount = 1,
highlightCount = 0,
)
val existingNotificationHighlighted = RoomAggregateNotificationCount(
notificationCount = 1,
highlightCount = 1,
)
every { fakeGetSpacesUseCase.execute(any()) } returns
flowOf(noSpacesInvite, existingSpaceInvite, existingSpaceInvite, noSpacesInvite, noSpacesInvite)
every { fakeGetNotificationCountForSpacesUseCase.execute(any()) } returns
flowOf(noNotification, noNotification, existingNotificationNotHighlighted, existingNotificationNotHighlighted, existingNotificationHighlighted)
// When
val testObserver = getSpacesNotificationBadgeStateUseCase.execute().test(this)
advanceUntilIdle()
// Then
val expectedState1 = UnreadCounterBadgeView.State.Count(count = 0, highlighted = false)
val expectedState2 = UnreadCounterBadgeView.State.Text(text = "!", highlighted = true)
val expectedState3 = UnreadCounterBadgeView.State.Count(count = 1, highlighted = true)
val expectedState4 = UnreadCounterBadgeView.State.Count(count = 1, highlighted = false)
val expectedState5 = UnreadCounterBadgeView.State.Count(count = 1, highlighted = true)
testObserver
.assertValues(expectedState1, expectedState2, expectedState3, expectedState4, expectedState5)
.finish()
verify {
fakeGetSpacesUseCase.execute(match {
it.memberships == listOf(Membership.INVITE) && it.displayName == QueryStringValue.IsNotEmpty
})
}
verify { fakeGetNotificationCountForSpacesUseCase.execute(SpaceFilter.NoFilter) }
}
}

View File

@ -0,0 +1,66 @@
/*
* 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 im.vector.app.features.home
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.test.test
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Rule
import org.junit.Test
internal class NewHomeDetailViewModelTest {
@get:Rule
val mavericksTestRule = MavericksTestRule(testDispatcher = UnconfinedTestDispatcher())
private val initialState = NewHomeDetailViewState()
private val fakeGetSpacesNotificationBadgeStateUseCase = mockk<GetSpacesNotificationBadgeStateUseCase>()
private fun createViewModel(): NewHomeDetailViewModel {
return NewHomeDetailViewModel(
initialState = initialState,
getSpacesNotificationBadgeStateUseCase = fakeGetSpacesNotificationBadgeStateUseCase,
)
}
@Test
fun `given the viewModel is created then viewState is updated with space notifications badge state`() {
// Given
val aBadgeState = UnreadCounterBadgeView.State.Count(count = 1, highlighted = false)
every { fakeGetSpacesNotificationBadgeStateUseCase.execute() } returns flowOf(aBadgeState)
// When
val viewModel = createViewModel()
val viewModelTest = viewModel.test()
// Then
val expectedViewState = initialState.copy(
spacesNotificationCounterBadgeState = aBadgeState,
)
viewModelTest
.assertLatestState(expectedViewState)
.finish()
verify {
fakeGetSpacesNotificationBadgeStateUseCase.execute()
}
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 im.vector.app.features.spaces
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.givenAsFlow
import im.vector.app.test.test
import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
internal class GetSpacesUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val getSpacesUseCase = GetSpacesUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
)
@Before
fun setUp() {
fakeFlowLiveDataConversions.setup()
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given params when execute then the list of summaries is returned`() = runTest {
// Given
val queryParams = givenSpaceQueryParams()
val firstSummaries = listOf<RoomSummary>(mockk())
val nextSummaries = listOf<RoomSummary>(mockk())
fakeActiveSessionHolder.fakeSession
.fakeSpaceService
.givenGetSpaceSummariesReturns(firstSummaries)
fakeActiveSessionHolder.fakeSession
.fakeSpaceService
.givenGetSpaceSummariesLiveReturns(nextSummaries)
.givenAsFlow()
// When
val testObserver = getSpacesUseCase.execute(queryParams).test(this)
advanceUntilIdle()
// Then
testObserver
.assertValues(firstSummaries, nextSummaries)
.finish()
verify {
fakeActiveSessionHolder.fakeSession.fakeSpaceService.getSpaceSummaries(queryParams)
fakeActiveSessionHolder.fakeSession.fakeSpaceService.getSpaceSummariesLive(queryParams)
}
}
@Test
fun `given no active session when execute then empty flow is returned`() = runTest {
// Given
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
val queryParams = givenSpaceQueryParams()
// When
val testObserver = getSpacesUseCase.execute(queryParams).test(this)
advanceUntilIdle()
// Then
testObserver
.assertNoValues()
.finish()
verify(inverse = true) {
fakeActiveSessionHolder.fakeSession.fakeSpaceService.getSpaceSummaries(queryParams)
fakeActiveSessionHolder.fakeSession.fakeSpaceService.getSpaceSummariesLive(queryParams)
}
}
private fun givenSpaceQueryParams(): SpaceSummaryQueryParams {
return mockk()
}
}

View File

@ -0,0 +1,174 @@
/*
* 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 im.vector.app.features.spaces.notification
import androidx.paging.PagedList
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeAutoAcceptInvites
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.givenAsFlow
import im.vector.app.test.test
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.room.RoomSortOrder
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
internal class GetNotificationCountForSpacesUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val fakeAutoAcceptInvites = FakeAutoAcceptInvites()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val getNotificationCountForSpacesUseCase = GetNotificationCountForSpacesUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
autoAcceptInvites = fakeAutoAcceptInvites,
)
@Before
fun setUp() {
fakeFlowLiveDataConversions.setup()
mockkStatic("kotlinx.coroutines.flow.FlowKt")
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given space filter and auto accept invites when execute then correct notification count is returned`() = runTest {
// given
val spaceFilter = SpaceFilter.NoFilter
val pagedList = mockk<PagedList<RoomSummary>>()
val pagedListFlow = fakeActiveSessionHolder.fakeSession
.fakeRoomService
.givenGetPagedRoomSummariesLiveReturns(pagedList)
.givenAsFlow()
every { pagedListFlow.sample(any<Long>()) } returns pagedListFlow
val expectedNotificationCount = RoomAggregateNotificationCount(
notificationCount = 1,
highlightCount = 0,
)
fakeActiveSessionHolder.fakeSession
.fakeRoomService
.givenGetNotificationCountForRoomsReturns(expectedNotificationCount)
fakeAutoAcceptInvites._isEnabled = true
// When
val testObserver = getNotificationCountForSpacesUseCase.execute(spaceFilter).test(this)
advanceUntilIdle()
// Then
testObserver
.assertValues(expectedNotificationCount)
.finish()
verify {
fakeActiveSessionHolder.fakeSession.fakeRoomService.getNotificationCountForRooms(
queryParams = match { it.memberships == listOf(Membership.JOIN) && it.spaceFilter == spaceFilter }
)
fakeActiveSessionHolder.fakeSession.fakeRoomService.getPagedRoomSummariesLive(
queryParams = match { it.memberships == listOf(Membership.JOIN) && it.spaceFilter == spaceFilter },
pagedListConfig = any(),
sortOrder = RoomSortOrder.NONE,
)
}
}
@Test
fun `given space filter and show invites when execute then correct notification count is returned`() = runTest {
// given
val spaceFilter = SpaceFilter.NoFilter
val pagedList = mockk<PagedList<RoomSummary>>()
val pagedListFlow = fakeActiveSessionHolder.fakeSession
.fakeRoomService
.givenGetPagedRoomSummariesLiveReturns(pagedList)
.givenAsFlow()
every { pagedListFlow.sample(any<Long>()) } returns pagedListFlow
val notificationCount = RoomAggregateNotificationCount(
notificationCount = 1,
highlightCount = 0,
)
fakeActiveSessionHolder.fakeSession
.fakeRoomService
.givenGetNotificationCountForRoomsReturns(notificationCount)
val invitedRooms = listOf<RoomSummary>(mockk())
fakeActiveSessionHolder.fakeSession
.fakeRoomService
.givenGetRoomSummaries(invitedRooms)
fakeAutoAcceptInvites._isEnabled = false
val expectedNotificationCount = RoomAggregateNotificationCount(
notificationCount = notificationCount.notificationCount + invitedRooms.size,
highlightCount = notificationCount.highlightCount + invitedRooms.size,
)
// When
val testObserver = getNotificationCountForSpacesUseCase.execute(spaceFilter).test(this)
advanceUntilIdle()
// Then
testObserver
.assertValues(expectedNotificationCount)
.finish()
verify {
fakeActiveSessionHolder.fakeSession.fakeRoomService.getRoomSummaries(
queryParams = match { it.memberships == listOf(Membership.INVITE) }
)
fakeActiveSessionHolder.fakeSession.fakeRoomService.getNotificationCountForRooms(
queryParams = match { it.memberships == listOf(Membership.JOIN) && it.spaceFilter == spaceFilter }
)
fakeActiveSessionHolder.fakeSession.fakeRoomService.getPagedRoomSummariesLive(
queryParams = match { it.memberships == listOf(Membership.JOIN) && it.spaceFilter == spaceFilter },
pagedListConfig = any(),
sortOrder = RoomSortOrder.NONE,
)
}
}
@Test
fun `given no active session when execute then empty flow is returned`() = runTest {
// given
val spaceFilter = SpaceFilter.NoFilter
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
// When
val testObserver = getNotificationCountForSpacesUseCase.execute(spaceFilter).test(this)
// Then
testObserver
.assertNoValues()
.finish()
verify(inverse = true) {
fakeActiveSessionHolder.fakeSession.fakeRoomService.getPagedRoomSummariesLive(
queryParams = any(),
pagedListConfig = any(),
sortOrder = any(),
)
}
}
}

View File

@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import io.mockk.every
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class FakeFlowLiveDataConversions {
@ -28,6 +29,8 @@ class FakeFlowLiveDataConversions {
}
}
fun <T> LiveData<T>.givenAsFlow() {
every { asFlow() } returns flowOf(value!!)
fun <T> LiveData<T>.givenAsFlow(): Flow<T> {
return flowOf(value!!).also {
every { asFlow() } returns it
}
}

View File

@ -16,10 +16,14 @@
package im.vector.app.test.fakes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedList
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
class FakeRoomService(
private val fakeRoom: FakeRoom = FakeRoom()
@ -34,4 +38,18 @@ class FakeRoomService(
fun set(roomSummary: RoomSummary?) {
every { getRoomSummary(any()) } returns roomSummary
}
fun givenGetPagedRoomSummariesLiveReturns(pagedList: PagedList<RoomSummary>): LiveData<PagedList<RoomSummary>> {
return MutableLiveData(pagedList).also {
every { getPagedRoomSummariesLive(queryParams = any(), pagedListConfig = any(), sortOrder = any()) } returns it
}
}
fun givenGetNotificationCountForRoomsReturns(roomAggregateNotificationCount: RoomAggregateNotificationCount) {
every { getNotificationCountForRooms(queryParams = any()) } returns roomAggregateNotificationCount
}
fun givenGetRoomSummaries(roomSummaries: List<RoomSummary>) {
every { getRoomSummaries(any()) } returns roomSummaries
}
}

View File

@ -46,6 +46,7 @@ class FakeSession(
val fakeUserService: FakeUserService = FakeUserService(),
private val fakeEventService: FakeEventService = FakeEventService(),
val fakeSessionAccountDataService: FakeSessionAccountDataService = FakeSessionAccountDataService(),
val fakeSpaceService: FakeSpaceService = FakeSpaceService(),
) : Session by mockk(relaxed = true) {
init {
@ -66,6 +67,7 @@ class FakeSession(
override fun pushersService() = fakePushersService
override fun accountDataService() = fakeSessionAccountDataService
override fun userService() = fakeUserService
override fun spaceService() = fakeSpaceService
fun givenVectorStore(vectorSessionStore: VectorSessionStore) {
coEvery {

View File

@ -0,0 +1,37 @@
/*
* 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 im.vector.app.test.fakes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.mockk.every
import io.mockk.mockk
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.space.SpaceService
class FakeSpaceService : SpaceService by mockk() {
fun givenGetSpaceSummariesLiveReturns(roomSummaries: List<RoomSummary>): LiveData<List<RoomSummary>> {
return MutableLiveData(roomSummaries).also {
every { getSpaceSummariesLive(any()) } returns it
}
}
fun givenGetSpaceSummariesReturns(roomSummaries: List<RoomSummary>) {
every { getSpaceSummaries(any()) } returns roomSummaries
}
}