diff --git a/build.gradle b/build.gradle index ecd1cd557b..285713cacf 100644 --- a/build.gradle +++ b/build.gradle @@ -26,10 +26,10 @@ buildscript { classpath libs.gradle.hiltPlugin classpath 'com.google.firebase:firebase-appdistribution-gradle:3.2.0' classpath 'com.google.gms:google-services:4.3.15' - classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.5.0.2730' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.0.0.2929' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' classpath "com.likethesalad.android:stem-plugin:2.3.0" - classpath 'org.owasp:dependency-check-gradle:8.1.0' + classpath 'org.owasp:dependency-check-gradle:8.1.2' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.20" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' diff --git a/changelog.d/8157.feature b/changelog.d/8157.feature new file mode 100644 index 0000000000..3cab2b600b --- /dev/null +++ b/changelog.d/8157.feature @@ -0,0 +1 @@ +Add aggregated unread indicator for spaces in the new layout diff --git a/changelog.d/8168.bugfix b/changelog.d/8168.bugfix new file mode 100644 index 0000000000..39baa3f60a --- /dev/null +++ b/changelog.d/8168.bugfix @@ -0,0 +1 @@ +Fix timeline loading a wrong room on permalink if a matching event id is found in a different room diff --git a/changelog.d/8171.bugfix b/changelog.d/8171.bugfix new file mode 100644 index 0000000000..799fecabaf --- /dev/null +++ b/changelog.d/8171.bugfix @@ -0,0 +1 @@ +[Rich text editor] Fix code appearance \ No newline at end of file diff --git a/changelog.d/8190.bugfix b/changelog.d/8190.bugfix new file mode 100644 index 0000000000..95790f7c0c --- /dev/null +++ b/changelog.d/8190.bugfix @@ -0,0 +1 @@ +[Poll history] Fixing small issue about font style diff --git a/dependencies.gradle b/dependencies.gradle index 67bb90eb43..a28f54e9f5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -11,17 +11,17 @@ def gradle = "7.4.1" def kotlin = "1.8.10" def kotlinCoroutines = "1.6.4" def dagger = "2.45" -def firebaseBom = "31.2.1" +def firebaseBom = "31.2.2" def appDistribution = "16.0.0-beta05" def retrofit = "2.9.0" def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.182.0" +def flipper = "0.183.0" def epoxy = "5.0.0" def mavericks = "3.0.1" -def glide = "4.14.2" +def glide = "4.15.0" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert @@ -59,7 +59,7 @@ ext.libs = [ 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", 'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", - 'work' : "androidx.work:work-runtime-ktx:2.7.1", + 'work' : "androidx.work:work-runtime-ktx:2.8.0", 'autoFill' : "androidx.autofill:autofill:1.1.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.2.0", 'junit' : "androidx.test.ext:junit:1.1.5", @@ -70,7 +70,7 @@ ext.libs = [ 'datastore' : "androidx.datastore:datastore:1.0.0", 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", - 'coreTesting' : "androidx.arch.core:core-testing:2.1.0", + 'coreTesting' : "androidx.arch.core:core-testing:2.2.0", 'testCore' : "androidx.test:core:$androidxTest", 'orchestrator' : "androidx.test:orchestrator:$androidxOrchestrator", 'testRunner' : "androidx.test:runner:$androidxTest", @@ -79,7 +79,7 @@ ext.libs = [ 'espressoContrib' : "androidx.test.espresso:espresso-contrib:$espresso", 'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso", 'viewpager2' : "androidx.viewpager2:viewpager2:1.0.0", - 'transition' : "androidx.transition:transition:1.2.0", + 'transition' : "androidx.transition:transition:1.4.1", ], google : [ 'material' : "com.google.android.material:material:1.8.0", @@ -88,7 +88,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.6" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.7" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -103,7 +103,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:1.0.0" + 'wysiwyg' : "io.element.android:wysiwyg:1.1.1" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/library/external/jsonviewer/build.gradle b/library/external/jsonviewer/build.gradle index a5d297b860..7b3b62c082 100644 --- a/library/external/jsonviewer/build.gradle +++ b/library/external/jsonviewer/build.gradle @@ -65,7 +65,7 @@ dependencies { implementation libs.jetbrains.coroutinesCore implementation libs.jetbrains.coroutinesAndroid - testImplementation 'org.json:json:20220924' + testImplementation 'org.json:json:20230227' testImplementation libs.tests.junit androidTestImplementation libs.androidx.junit androidTestImplementation libs.androidx.espressoCore diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt index 7999a2ea14..a100741452 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt @@ -297,7 +297,7 @@ internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUser val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return val readReceiptChunk = ChunkEntity - .findIncludingEvent(realm, readReceipt) ?: return + .findIncludingEvent(realm, roomId, readReceipt) ?: return val readReceiptChunkThreadEvents = readReceiptChunk .timelineEvents diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt index 1e5d96b496..08c8bcf86e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt @@ -72,15 +72,16 @@ internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: .findFirst() } -internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List): RealmResults { +internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, roomId: String, eventIds: List): RealmResults { return realm.where() + .equalTo(ChunkEntityFields.ROOM.ROOM_ID, roomId) .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID) .findAll() } -internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: String): ChunkEntity? { - return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() +internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, roomId: String, eventId: String): ChunkEntity? { + return findAllIncludingEvents(realm, roomId, listOf(eventId)).firstOrNull() } internal fun ChunkEntity.Companion.create( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index ebfe23105e..0cc4abcb3d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -76,11 +76,11 @@ private fun hasReadMissingEvent(realm: Realm, userId: String, eventId: String, threadId: String? = ReadService.THREAD_ID_MAIN): Boolean { - return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId) + return realm.doesEventExistInChunkHistory(roomId, eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId) } -private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean { - return ChunkEntity.findIncludingEvent(this, eventId) != null +private fun Realm.doesEventExistInChunkHistory(roomId: String, eventId: String): Boolean { + return ChunkEntity.findIncludingEvent(this, roomId, eventId) != null } private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt index 9d8d8ecbf1..b73dd8160b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt @@ -59,7 +59,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor( ?: throw IllegalStateException("No token found") monarchy.awaitTransaction { realm -> - val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.lastKnownEventId) + val chunkToUpdate = ChunkEntity.findIncludingEvent(realm, params.roomId, params.lastKnownEventId) if (params.direction == PaginationDirection.FORWARDS) { chunkToUpdate?.nextToken = fromToken } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 6654eeadfc..2143ac1d21 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -278,7 +278,7 @@ internal class LoadTimelineStrategy constructor( .findAll() } is Mode.Permalink -> { - ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId)) + ChunkEntity.findAllIncludingEvents(realm, roomId, listOf(mode.originEventId)) } is Mode.Thread -> { recreateThreadChunkEntity(realm, mode.rootThreadEventId) diff --git a/vector/build.gradle b/vector/build.gradle index e58bf4095f..72f38f2928 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -199,7 +199,7 @@ dependencies { implementation 'com.github.hyuwah:DraggableView:1.0.0' // Custom Tab - implementation 'androidx.browser:browser:1.4.0' + implementation 'androidx.browser:browser:1.5.0' // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.7.0' diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 35d8d0e896..27981c3d36 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -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<*, *> } diff --git a/vector/src/main/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCase.kt b/vector/src/main/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCase.kt new file mode 100644 index 0000000000..62d1501dab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCase.kt @@ -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 { + 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, + ): 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, + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 3189c2b99e..ef855ff15b 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewModel.kt new file mode 100644 index 0000000000..67b4645944 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewModel.kt @@ -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(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: NewHomeDetailViewState): NewHomeDetailViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + init { + observeSpacesNotificationBadgeState() + } + + private fun observeSpacesNotificationBadgeState() { + getSpacesNotificationBadgeStateUseCase.execute() + .onEach { badgeState -> setState { copy(spacesNotificationCounterBadgeState = badgeState) } } + .launchIn(viewModelScope) + } + + override fun handle(action: EmptyAction) { + // do nothing + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewState.kt new file mode 100644 index 0000000000..7e368fb2d1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailViewState.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/spaces/GetSpacesUseCase.kt b/vector/src/main/java/im/vector/app/features/spaces/GetSpacesUseCase.kt new file mode 100644 index 0000000000..048e4b01bd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/GetSpacesUseCase.kt @@ -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> { + val session = activeSessionHolder.getSafeActiveSession() + + return session?.flow()?.liveSpaceSummaries(params) ?: emptyFlow() + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt index 99f6a254b8..b2776e5f87 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt @@ -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(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() diff --git a/vector/src/main/java/im/vector/app/features/spaces/notification/GetNotificationCountForSpacesUseCase.kt b/vector/src/main/java/im/vector/app/features/spaces/notification/GetNotificationCountForSpacesUseCase.kt new file mode 100644 index 0000000000..08d63e2653 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/notification/GetNotificationCountForSpacesUseCase.kt @@ -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 { + 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() + } +} diff --git a/vector/src/main/res/drawable/bg_code_block.xml b/vector/src/main/res/drawable/bg_code_block.xml new file mode 100644 index 0000000000..c492ce02c2 --- /dev/null +++ b/vector/src/main/res/drawable/bg_code_block.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/bg_inline_code_multi_line_left.xml b/vector/src/main/res/drawable/bg_inline_code_multi_line_left.xml new file mode 100644 index 0000000000..fa5b052562 --- /dev/null +++ b/vector/src/main/res/drawable/bg_inline_code_multi_line_left.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/bg_inline_code_multi_line_mid.xml b/vector/src/main/res/drawable/bg_inline_code_multi_line_mid.xml new file mode 100644 index 0000000000..0f55cbbbbb --- /dev/null +++ b/vector/src/main/res/drawable/bg_inline_code_multi_line_mid.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/vector/src/main/res/drawable/bg_inline_code_multi_line_right.xml b/vector/src/main/res/drawable/bg_inline_code_multi_line_right.xml new file mode 100644 index 0000000000..0f0d004869 --- /dev/null +++ b/vector/src/main/res/drawable/bg_inline_code_multi_line_right.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/bg_inline_code_single_line.xml b/vector/src/main/res/drawable/bg_inline_code_single_line.xml new file mode 100644 index 0000000000..e70625152a --- /dev/null +++ b/vector/src/main/res/drawable/bg_inline_code_single_line.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/vector/src/main/res/layout/composer_rich_text_layout.xml b/vector/src/main/res/layout/composer_rich_text_layout.xml index 8992b632c0..f5ad5c51db 100644 --- a/vector/src/main/res/layout/composer_rich_text_layout.xml +++ b/vector/src/main/res/layout/composer_rich_text_layout.xml @@ -126,6 +126,11 @@ app:layout_constraintTop_toBottomOf="@id/composerModeBarrier" app:bulletRadius="4sp" app:bulletGap="8sp" + app:codeBlockBackgroundDrawable="@drawable/bg_code_block" + app:inlineCodeSingleLineBg="@drawable/bg_inline_code_single_line" + app:inlineCodeMultiLineBgLeft="@drawable/bg_inline_code_multi_line_left" + app:inlineCodeMultiLineBgMid="@drawable/bg_inline_code_multi_line_mid" + app:inlineCodeMultiLineBgRight="@drawable/bg_inline_code_multi_line_right" tools:text="@tools:sample/lorem/random" /> + + diff --git a/vector/src/test/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCaseTest.kt new file mode 100644 index 0000000000..4d7d0d98f4 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/GetSpacesNotificationBadgeStateUseCaseTest.kt @@ -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() + private val fakeGetSpacesUseCase = mockk() + + 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() + val existingSpaceInvite = listOf(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) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/NewHomeDetailViewModelTest.kt b/vector/src/test/java/im/vector/app/features/home/NewHomeDetailViewModelTest.kt new file mode 100644 index 0000000000..39adc0a811 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/NewHomeDetailViewModelTest.kt @@ -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() + + 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() + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/spaces/GetSpacesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/spaces/GetSpacesUseCaseTest.kt new file mode 100644 index 0000000000..2e8d50ff3f --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/spaces/GetSpacesUseCaseTest.kt @@ -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(mockk()) + val nextSummaries = listOf(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() + } +} diff --git a/vector/src/test/java/im/vector/app/features/spaces/notification/GetNotificationCountForSpacesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/spaces/notification/GetNotificationCountForSpacesUseCaseTest.kt new file mode 100644 index 0000000000..8d4bdb1b30 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/spaces/notification/GetNotificationCountForSpacesUseCaseTest.kt @@ -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>() + val pagedListFlow = fakeActiveSessionHolder.fakeSession + .fakeRoomService + .givenGetPagedRoomSummariesLiveReturns(pagedList) + .givenAsFlow() + every { pagedListFlow.sample(any()) } 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>() + val pagedListFlow = fakeActiveSessionHolder.fakeSession + .fakeRoomService + .givenGetPagedRoomSummariesLiveReturns(pagedList) + .givenAsFlow() + every { pagedListFlow.sample(any()) } returns pagedListFlow + val notificationCount = RoomAggregateNotificationCount( + notificationCount = 1, + highlightCount = 0, + ) + fakeActiveSessionHolder.fakeSession + .fakeRoomService + .givenGetNotificationCountForRoomsReturns(notificationCount) + val invitedRooms = listOf(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(), + ) + } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt index 956a86f32e..cdbe828521 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt @@ -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 LiveData.givenAsFlow() { - every { asFlow() } returns flowOf(value!!) +fun LiveData.givenAsFlow(): Flow { + return flowOf(value!!).also { + every { asFlow() } returns it + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt index e957266383..63209222b2 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt @@ -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): LiveData> { + 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) { + every { getRoomSummaries(any()) } returns roomSummaries + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index 1b6d3e2729..ada23c159e 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -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 { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSpaceService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSpaceService.kt new file mode 100644 index 0000000000..59c7d5524d --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSpaceService.kt @@ -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): LiveData> { + return MutableLiveData(roomSummaries).also { + every { getSpaceSummariesLive(any()) } returns it + } + } + + fun givenGetSpaceSummariesReturns(roomSummaries: List) { + every { getSpaceSummaries(any()) } returns roomSummaries + } +}