diff --git a/CHANGES.md b/CHANGES.md index cac9ab26086..5bd85efc675 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,34 @@ +Changes in Element v.5.16 (2022-12-29) +====================================== + +Features ✨ +---------- + - [Rich text editor] Add support for links ([#7746](https://github.com/vector-im/element-android/issues/7746)) + - [Poll] When a poll is ended, use /relations API to ensure poll results are correct ([#7767](https://github.com/vector-im/element-android/issues/7767)) + - [Session manager] Security recommendations cards: whole view should be tappable ([#7795](https://github.com/vector-im/element-android/issues/7795)) + - [Session manager] Other sessions list: header should not be sticky ([#7797](https://github.com/vector-im/element-android/issues/7797)) + +Bugfixes 🐛 +---------- + - Do not show typing notification of ignored users. ([#2965](https://github.com/vector-im/element-android/issues/2965)) + - [Push Notifications, Threads] - quick reply to threaded notification now sent to thread except main timeline ([#7475](https://github.com/vector-im/element-android/issues/7475)) + - [Session manager] Other sessions list: filter option is displayed when selection mode is enabled ([#7784](https://github.com/vector-im/element-android/issues/7784)) + - [Session manager] Other sessions: Filter bottom sheet cut in landscape mode ([#7786](https://github.com/vector-im/element-android/issues/7786)) + - Automatically show keyboard after learn more bottom sheet is dismissed ([#7790](https://github.com/vector-im/element-android/issues/7790)) + - [Session Manager] Other sessions list: cannot select/deselect session by a long press when in select mode ([#7792](https://github.com/vector-im/element-android/issues/7792)) + - Fix current session ip address visibility ([#7794](https://github.com/vector-im/element-android/issues/7794)) + - Device Manager UI review fixes ([#7798](https://github.com/vector-im/element-android/issues/7798)) + +SDK API changes ⚠️ +------------------ + - [Sync] Sync Filter params are moved to MatrixConfiguration and will not be stored in session realm to avoid bug when session cache is cleared ([#7843](https://github.com/vector-im/element-android/issues/7843)) + +Other changes +------------- + - [Voice Broadcast] Replace the player timeline ([#7821](https://github.com/vector-im/element-android/issues/7821)) + - Increase session manager test coverage ([#7836](https://github.com/vector-im/element-android/issues/7836)) + + Changes in Element v1.5.14 (2022-12-20) ======================================= diff --git a/dependencies.gradle b/dependencies.gradle index dbb5f5fe053..b81c1c20179 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -26,7 +26,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.9.0" +def sentry = "6.9.2" def fragment = "1.5.5" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 @@ -83,7 +83,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.1" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.3" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -98,7 +98,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.9.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.10.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", @@ -129,7 +129,7 @@ ext.libs = [ 'mavericksTesting' : "com.airbnb.android:mavericks-testing:$mavericks" ], maplibre : [ - 'androidSdk' : "org.maplibre.gl:android-sdk:9.5.2", + 'androidSdk' : "org.maplibre.gl:android-sdk:9.6.0", 'pluginAnnotation' : "org.maplibre.gl:android-plugin-annotation-v9:1.0.0" ], mockk : [ diff --git a/fastlane/metadata/android/en-US/changelogs/40105160.txt b/fastlane/metadata/android/en-US/changelogs/40105160.txt new file mode 100644 index 00000000000..91c25cf053b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Main changes in this version: Thread are now enabled by default. +Full changelog: https://github.com/vector-im/element-android/releases diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index d37b5f09067..73cb60bb689 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -419,6 +419,7 @@ Got it Select all Deselect all + Yes, Stop Copied to clipboard @@ -3120,6 +3121,8 @@ You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. %1$s left + Stop live broadcasting? + Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room. Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. @@ -3335,7 +3338,7 @@ Consider signing out from old sessions (%1$d day or more) that you don’t use anymore. Consider signing out from old sessions (%1$d days or more) that you don’t use anymore. - Current Session + Current session Session Device @@ -3476,13 +3479,19 @@ Confirm Please ensure that you know the origin of this code. By linking devices, you will provide someone with full access to your account. - + Apply bold format Apply italic format Apply strikethrough format Apply underline format + Set link Toggle full screen mode + Text + Link + Create a link + Edit link + In reply to sent a file. diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 4558f4e8b58..f839a6c2633 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.14\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.16\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 8edecb273d1..eeb2def5827 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -50,7 +50,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder import timber.log.Timber import java.util.UUID import java.util.concurrent.CountDownLatch @@ -347,10 +346,6 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig: assertTrue(registrationResult is RegistrationResult.Success) val session = (registrationResult as RegistrationResult.Success).session session.open() - session.filterService().setSyncFilter( - SyncFilterBuilder() - .lazyLoadMembersForStateEvents(true) - ) if (sessionTestParams.withInitialSync) { syncSession(session, 120_000) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt index a9753e2407f..84650da72f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt @@ -16,9 +16,13 @@ package org.matrix.android.sdk.api +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams + data class SyncConfig( /** * Time to keep sync connection alive for before making another request in milliseconds. */ val longPollTimeout: Long = 30_000L, + + val syncFilterParams: SyncFilterParams = SyncFilterParams() ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 13993149f4e..cf0f4bdce0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -50,7 +50,6 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService import org.matrix.android.sdk.api.session.statistics.StatisticsListener -import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncService import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService @@ -163,11 +162,6 @@ interface Session { */ fun signOutService(): SignOutService - /** - * Returns the FilterService associated with the session. - */ - fun filterService(): FilterService - /** * Returns the PushRuleService associated with the session. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 40ce6ecb5c1..9b5f4ac19f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -388,7 +388,13 @@ fun Event.isLocationMessage(): Boolean { } } -fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values +fun Event.isPoll(): Boolean = isPollStart() || isPollEnd() + +fun Event.isPollStart(): Boolean = getClearType() in EventType.POLL_START.values + +fun Event.isPollResponse(): Boolean = getClearType() in EventType.POLL_RESPONSE.values + +fun Event.isPollEnd(): Boolean = getClearType() in EventType.POLL_END.values fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterParams.kt similarity index 91% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterParams.kt index a7de7f55796..02c5b0f8efd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterParams.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.sync.filter +package org.matrix.android.sdk.api.session.sync.filter -internal data class SyncFilterParams( +data class SyncFilterParams( val lazyLoadMembersForStateEvents: Boolean? = null, val lazyLoadMembersForMessageEvents: Boolean? = null, val useThreadNotifications: Boolean? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index bc3309132a8..c9eabeab480 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -24,10 +24,12 @@ import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent @@ -85,6 +87,27 @@ internal class EventDecryptor @Inject constructor( return internalDecryptEvent(event, timeline) } + /** + * Decrypt an event and save the result in the given event. + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + */ + suspend fun decryptEventAndSaveResult(event: Event, timeline: String) { + tryOrNull(message = "Unable to decrypt the event") { + decryptEvent(event, timeline) + } + ?.let { result -> + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, + isSafe = result.isSafe + ) + } + } + /** * Decrypt an event asynchronously. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 5295abffe32..ba102a7a48c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -63,6 +63,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -71,7 +72,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 46L, + schemaVersion = 47L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -127,5 +128,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 44) MigrateSessionTo044(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform() if (oldVersion < 46) MigrateSessionTo046(realm).perform() + if (oldVersion < 47) MigrateSessionTo047(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt deleted file mode 100644 index 645cb41af5c..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.database.mapper - -import io.realm.RealmList -import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity -import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams -import javax.inject.Inject - -internal class FilterParamsMapper @Inject constructor() { - - fun map(entity: SyncFilterParamsEntity): SyncFilterParams { - val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) { - entity.listOfSupportedEventTypes?.toList() - } else { - null - } - val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) { - entity.listOfSupportedStateEventTypes?.toList() - } else { - null - } - return SyncFilterParams( - useThreadNotifications = entity.useThreadNotifications, - lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents, - lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents, - listOfSupportedEventTypes = eventTypes, - listOfSupportedStateEventTypes = stateEventTypes, - ) - } - - fun map(params: SyncFilterParams): SyncFilterParamsEntity { - return SyncFilterParamsEntity( - useThreadNotifications = params.useThreadNotifications, - lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents, - lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents, - listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(), - listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null, - listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(), - listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null, - ) - } - - private fun List?.toRealmList(): RealmList? { - return this?.toTypedArray()?.let { RealmList(*it) } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo047.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo047.kt new file mode 100644 index 00000000000..5bfaaa760cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo047.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo047(realm: DynamicRealm) : RealmMigrator(realm, 47) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.remove("SyncFilterParamsEntity") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 0ab30657ed4..0d998e8fe15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -72,7 +72,6 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit SpaceParentSummaryEntity::class, UserPresenceEntity::class, ThreadSummaryEntity::class, - SyncFilterParamsEntity::class, ThreadListPageEntity::class ] ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 679c5085efc..1af904bbc74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -57,7 +57,6 @@ import org.matrix.android.sdk.api.session.search.SearchService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.space.SpaceService -import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncService import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService @@ -97,7 +96,6 @@ internal class DefaultSession @Inject constructor( private val roomService: Lazy, private val roomDirectoryService: Lazy, private val userService: Lazy, - private val filterService: Lazy, private val federationService: Lazy, private val cacheService: Lazy, private val signOutService: Lazy, @@ -209,7 +207,6 @@ internal class DefaultSession @Inject constructor( override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService.get() override fun userService(): UserService = userService.get() override fun signOutService(): SignOutService = signOutService.get() - override fun filterService(): FilterService = filterService.get() override fun pushRuleService(): PushRuleService = pushRuleService.get() override fun pushersService(): PushersService = pushersService.get() override fun eventService(): EventService = eventService.get() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt index 4e5b005584f..f70b4d17996 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt @@ -17,20 +17,15 @@ package org.matrix.android.sdk.internal.session.filter import com.zhuinden.monarchy.Monarchy -import io.realm.kotlin.where -import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper import org.matrix.android.sdk.internal.database.model.FilterEntity -import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams import org.matrix.android.sdk.internal.util.awaitTransaction import javax.inject.Inject internal class DefaultFilterRepository @Inject constructor( @SessionDatabase private val monarchy: Monarchy, - private val filterParamsMapper: FilterParamsMapper ) : FilterRepository { override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) { @@ -69,19 +64,4 @@ internal class DefaultFilterRepository @Inject constructor( FilterEntity.getOrCreate(it).roomEventFilterJson } } - - override suspend fun getStoredFilterParams(): SyncFilterParams? { - return monarchy.awaitTransaction { realm -> - realm.where().findFirst()?.let { - filterParamsMapper.map(it) - } - } - } - - override suspend fun storeFilterParams(params: SyncFilterParams) { - return monarchy.awaitTransaction { realm -> - val entity = filterParamsMapper.map(params) - realm.insertOrUpdate(entity) - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt deleted file mode 100644 index c54e7de07ae..00000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.session.filter - -import org.matrix.android.sdk.api.session.sync.FilterService -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder -import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource -import javax.inject.Inject - -internal class DefaultFilterService @Inject constructor( - private val saveFilterTask: SaveFilterTask, - private val filterRepository: FilterRepository, - private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, -) : FilterService { - - // TODO Pass a list of support events instead - override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) { - filterRepository.storeFilterParams(filterBuilder.extractParams()) - - // don't upload/store filter until homeserver capabilities are fetched - homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities -> - saveFilterTask.execute( - SaveFilterTask.Params( - filter = filterBuilder.build(homeServerCapabilities) - ) - ) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt index ca9f798fd96..5ae2c2a47d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.filter import dagger.Binds import dagger.Module import dagger.Provides -import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.internal.session.SessionScope import retrofit2.Retrofit @@ -39,9 +38,6 @@ internal abstract class FilterModule { @Binds abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository - @Binds - abstract fun bindFilterService(service: DefaultFilterService): FilterService - @Binds abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt index 71d7391e87c..d0ec4b98bb2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt @@ -16,8 +16,6 @@ package org.matrix.android.sdk.internal.session.filter -import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams - /** * Repository for request filters. */ @@ -44,14 +42,4 @@ internal interface FilterRepository { * Return the room filter. */ suspend fun getRoomFilterBody(): String - - /** - * Returns filter params stored in local storage if it exists. - */ - suspend fun getStoredFilterParams(): SyncFilterParams? - - /** - * Stores filter params to local storage. - */ - suspend fun storeFilterParams(params: SyncFilterParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt index 76805c5c514..5c7027f8b3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt @@ -16,9 +16,10 @@ package org.matrix.android.sdk.internal.session.filter +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource +import org.matrix.android.sdk.internal.sync.filter.SyncFilterBuilder import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -27,7 +28,8 @@ internal interface GetCurrentFilterTask : Task internal class DefaultGetCurrentFilterTask @Inject constructor( private val filterRepository: FilterRepository, private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource, - private val saveFilterTask: SaveFilterTask + private val saveFilterTask: SaveFilterTask, + private val matrixConfiguration: MatrixConfiguration ) : GetCurrentFilterTask { override suspend fun execute(params: Unit): String { @@ -35,7 +37,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor( val storedFilterBody = filterRepository.getStoredSyncFilterBody() val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities() val currentFilter = SyncFilterBuilder() - .with(filterRepository.getStoredFilterParams()) + .with(matrixConfiguration.syncConfig.syncFilterParams) .build(homeServerCapabilities) val currentFilterBody = currentFilter.toJSONString() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index ddb7d6a8e6b..34b6ee525d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomStrippedState import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams @@ -251,7 +250,7 @@ internal interface RoomAPI { * @param limit max number of Event to retrieve */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}") - suspend fun getRelations( + suspend fun getRelationsWithEventType( @Path("roomId") roomId: String, @Path("eventId") eventId: String, @Path("relationType") relationType: String, @@ -262,7 +261,7 @@ internal interface RoomAPI { ): RelationsResponse /** - * Paginate relations for thread events based in normal topological order. + * Paginate relations for events based in normal topological order. * * @param roomId the room Id * @param eventId the event Id @@ -272,10 +271,10 @@ internal interface RoomAPI { * @param limit max number of Event to retrieve */ @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}") - suspend fun getThreadsRelations( + suspend fun getRelations( @Path("roomId") roomId: String, @Path("eventId") eventId: String, - @Path("relationType") relationType: String = RelationType.THREAD, + @Path("relationType") relationType: String, @Query("from") from: String? = null, @Query("to") to: String? = null, @Query("limit") limit: Int? = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 1475b672767..c28d24995fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -99,6 +99,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask +import org.matrix.android.sdk.internal.session.room.relation.poll.DefaultFetchPollResponseEventsTask +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask @@ -354,4 +356,7 @@ internal abstract class RoomModule { @Binds abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask + + @Binds + abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 455ccabbc62..a424becbd68 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll import io.realm.Realm +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event @@ -40,9 +41,14 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask +import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject -class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationProcessor { +internal class DefaultPollAggregationProcessor @Inject constructor( + private val taskExecutor: TaskExecutor, + private val fetchPollResponseEventsTask: FetchPollResponseEventsTask, +) : PollAggregationProcessor { override fun handlePollStartEvent(realm: Realm, event: Event): Boolean { val content = event.getClearContent()?.toModel() @@ -174,6 +180,10 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) } + if (!isLocalEcho) { + ensurePollIsFullyAggregated(roomId, pollEventId) + } + return true } @@ -200,4 +210,20 @@ class DefaultPollAggregationProcessor @Inject constructor() : PollAggregationPro eventAnnotationsSummaryEntity.pollResponseSummary = it } } + + /** + * Check that all related votes to a given poll are all retrieved and aggregated. + */ + private fun ensurePollIsFullyAggregated( + roomId: String, + pollEventId: String + ) { + taskExecutor.executorScope.launch { + val params = FetchPollResponseEventsTask.Params( + roomId = roomId, + startPollEventId = pollEventId, + ) + fetchPollResponseEventsTask.execute(params) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt index 93c7f143fd3..50439f51ebb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -43,7 +43,7 @@ internal class DefaultFetchEditHistoryTask @Inject constructor( override suspend fun execute(params: FetchEditHistoryTask.Params): List { val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId) val response = executeRequest(globalErrorReceiver) { - roomAPI.getRelations( + roomAPI.getRelationsWithEventType( roomId = params.roomId, eventId = params.eventId, relationType = RelationType.REPLACE, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt new file mode 100644 index 00000000000..e7dd8c57eb3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room.relation.poll + +import androidx.annotation.VisibleForTesting +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.EventDecryptor +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock +import javax.inject.Inject + +@VisibleForTesting +const val FETCH_RELATED_EVENTS_LIMIT = 50 + +/** + * Task to fetch all the vote events to ensure full aggregation for a given poll. + */ +internal interface FetchPollResponseEventsTask : Task> { + data class Params( + val roomId: String, + val startPollEventId: String, + ) +} + +internal class DefaultFetchPollResponseEventsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val globalErrorReceiver: GlobalErrorReceiver, + @SessionDatabase private val monarchy: Monarchy, + private val clock: Clock, + private val eventDecryptor: EventDecryptor, +) : FetchPollResponseEventsTask { + + override suspend fun execute(params: FetchPollResponseEventsTask.Params): Result = runCatching { + var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params) + + while (nextBatch?.isNotEmpty() == true) { + nextBatch = fetchAndProcessRelatedEventsFrom(params, from = nextBatch) + } + } + + private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? { + val response = getRelatedEvents(params, from) + + val filteredEvents = response.chunks + .map { decryptEventIfNeeded(it) } + .filter { it.isPollResponse() } + + addMissingEventsInDB(params.roomId, filteredEvents) + + return response.nextBatch + } + + private suspend fun getRelatedEvents(params: FetchPollResponseEventsTask.Params, from: String? = null): RelationsResponse { + return executeRequest(globalErrorReceiver, canRetry = true) { + roomAPI.getRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = from, + limit = FETCH_RELATED_EVENTS_LIMIT, + ) + } + } + + private suspend fun addMissingEventsInDB(roomId: String, events: List) { + monarchy.awaitTransaction { realm -> + val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } + if (eventIdsToCheck.isNotEmpty()) { + val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } + + events.filterNot { it.eventId in existingIds } + .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } + .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + } + } + } + + private suspend fun decryptEventIfNeeded(event: Event): Event { + if (event.isEncrypted()) { + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") + } + + event.ageLocalTs = computeLocalTs(event) + + return event + } + + private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index 4cf6445920c..1e9a785c803 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.DefaultCryptoService @@ -102,11 +103,12 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( override suspend fun execute(params: FetchThreadTimelineTask.Params): Result { val response = executeRequest(globalErrorReceiver) { - roomAPI.getThreadsRelations( + roomAPI.getRelations( roomId = params.roomId, eventId = params.rootThreadEventId, + relationType = RelationType.THREAD, from = params.from, - limit = params.limit + limit = params.limit, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 69beb8d599a..8adfdc5dbbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -196,6 +196,11 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) + if (roomSummary?.joinedMembersCount == null) { + // in case m.joined_member_count from sync summary was null? + // better to use what we know + roomSummaryEntity.joinedMembersCount = otherRoomMembers.size + 1 + } if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) { if (aggregator == null) { // Do it now diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index e0751865ad5..3707205aefb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -16,8 +16,6 @@ package org.matrix.android.sdk.internal.session.room.timeline -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.network.GlobalErrorReceiver @@ -48,18 +46,7 @@ internal class DefaultGetEventTask @Inject constructor( // Try to decrypt the Event if (event.isEncrypted()) { - tryOrNull(message = "Unable to decrypt the event") { - eventDecryptor.decryptEvent(event, "") - } - ?.let { result -> - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain, - isSafe = result.isSafe - ) - } + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") } event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt index 54bb63753c9..519112b1b78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room import io.realm.Realm import org.matrix.android.sdk.api.session.room.sender.SenderInfo +import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker @@ -30,8 +31,15 @@ internal class RoomTypingUsersHandler @Inject constructor( // TODO This could be handled outside of the Realm transaction. Use the new aggregator? fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) { + val typingUserIds = ephemeralResult?.typingUserIds + if (typingUserIds.isNullOrEmpty()) { + typingUsersTracker.setTypingUsersFromRoom(roomId, emptyList()) + return + } + // Filter ignored users and current user + val filteredUserIds = realm.where(IgnoredUserEntity::class.java).findAll().map { it.userId } + userId val roomMemberHelper = RoomMemberHelper(realm, roomId) - val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId }.orEmpty() + val typingIds = typingUserIds.filter { it !in filteredUserIds } val senderInfo = typingIds.map { userId -> val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId) SenderInfo( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterBuilder.kt similarity index 89% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterBuilder.kt index ad55b26dfd4..d58b9d3765b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterBuilder.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.sync.filter +package org.matrix.android.sdk.internal.sync.filter import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams import org.matrix.android.sdk.internal.session.filter.Filter import org.matrix.android.sdk.internal.session.filter.RoomEventFilter import org.matrix.android.sdk.internal.session.filter.RoomFilter -import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams -class SyncFilterBuilder { +internal class SyncFilterBuilder { private var lazyLoadMembersForStateEvents: Boolean? = null private var lazyLoadMembersForMessageEvents: Boolean? = null private var useThreadNotifications: Boolean? = null @@ -54,16 +54,6 @@ class SyncFilterBuilder { } } - internal fun extractParams(): SyncFilterParams { - return SyncFilterParams( - useThreadNotifications = useThreadNotifications, - lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents, - lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents, - listOfSupportedEventTypes = listOfSupportedEventTypes, - listOfSupportedStateEventTypes = listOfSupportedStateEventTypes, - ) - } - internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter { return Filter( room = buildRoomFilter(homeServerCapabilities) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index c1fd615e254..0888d829079 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -16,9 +16,13 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.realm.RealmList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.Before @@ -34,6 +38,7 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_EVENT_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.AN_INVALID_POLL_RESPONSE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_BROKEN_POLL_REPLACE_EVENT +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_CONTENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_END_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REFERENCE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_POLL_REPLACE_EVENT @@ -43,13 +48,22 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_ROOM_ID import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1 +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask +import org.matrix.android.sdk.test.fakes.FakeFetchPollResponseEventsTask import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeTaskExecutor import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindFirst +@OptIn(ExperimentalCoroutinesApi::class) class DefaultPollAggregationProcessorTest { - private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor() + private val fakeTaskExecutor = FakeTaskExecutor() + private val fakeFetchPollResponseEventsTask = FakeFetchPollResponseEventsTask() + private val pollAggregationProcessor: PollAggregationProcessor = DefaultPollAggregationProcessor( + taskExecutor = fakeTaskExecutor.instance, + fetchPollResponseEventsTask = fakeFetchPollResponseEventsTask + ) private val realm = FakeRealm() private val session = mockk() @@ -114,16 +128,28 @@ class DefaultPollAggregationProcessorTest { } @Test - fun `given a poll end event, when processing, then is processed and return true`() { + fun `given a poll end event, when processing, then is processed and return true`() = runTest { + // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + every { fakeTaskExecutor.instance.executorScope } returns this + + // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) + + // Then pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() } @Test - fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() { + fun `given a poll end event for my own poll without enough redaction power level, when processing, then is processed and returns true`() = runTest { + // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + every { fakeTaskExecutor.instance.executorScope } returns this + + // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) + + // Then pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() } @@ -135,6 +161,28 @@ class DefaultPollAggregationProcessorTest { pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse() } + @Test + fun `given a non local echo poll end event, when is processed, then ensure to aggregate all poll responses`() = runTest { + // Given + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() + val powerLevelsHelper = mockRedactionPowerLevels("another-sender-id", true) + val event = A_POLL_END_EVENT.copy(senderId = "another-sender-id") + every { fakeTaskExecutor.instance.executorScope } returns this + val expectedParams = FetchPollResponseEventsTask.Params( + roomId = A_POLL_END_EVENT.roomId.orEmpty(), + startPollEventId = A_POLL_END_CONTENT.relatesTo?.eventId.orEmpty(), + ) + + // When + pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event) + advanceUntilIdle() + + // Then + coVerify { + fakeFetchPollResponseEventsTask.execute(expectedParams) + } + } + private fun mockEventAnnotationsSummaryEntity() { realm.givenWhere() .givenFindFirst(EventAnnotationsSummaryEntity()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt new file mode 100644 index 00000000000..8d50bac38f7 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room.relation.poll + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeEventDecryptor +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeRoomApi +import org.matrix.android.sdk.test.fakes.givenFindAll +import org.matrix.android.sdk.test.fakes.givenIn + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultFetchPollResponseEventsTaskTest { + + private val fakeRoomAPI = FakeRoomApi() + private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() + private val fakeMonarchy = FakeMonarchy() + private val fakeClock = FakeClock() + private val fakeEventDecryptor = FakeEventDecryptor() + + private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask( + roomAPI = fakeRoomAPI.instance, + globalErrorReceiver = fakeGlobalErrorReceiver, + monarchy = fakeMonarchy.instance, + clock = fakeClock, + eventDecryptor = fakeEventDecryptor.instance, + ) + + @Before + fun setup() { + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") + mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest { + // Given + val aRoomId = "roomId" + val aPollEventId = "eventId" + val params = givenTaskParams(roomId = aRoomId, eventId = aPollEventId) + val aNextBatchToken = "nextBatch" + val anEventId1 = "eventId1" + val anEventId2 = "eventId2" + val anEventId3 = "eventId3" + val anEventId4 = "eventId4" + val event1 = givenAnEvent(eventId = anEventId1, isPollResponse = true, isEncrypted = true) + val event2 = givenAnEvent(eventId = anEventId2, isPollResponse = true, isEncrypted = true) + val event3 = givenAnEvent(eventId = anEventId3, isPollResponse = false, isEncrypted = false) + val event4 = givenAnEvent(eventId = anEventId4, isPollResponse = false, isEncrypted = false) + val firstEvents = listOf(event1, event2) + val secondEvents = listOf(event3, event4) + val firstResponse = givenARelationsResponse(events = firstEvents, nextBatch = aNextBatchToken) + fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse) + val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null) + fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) + fakeClock.givenEpoch(123) + givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) + val eventEntityToSave = EventEntity(eventId = anEventId2) + every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave + every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + + // When + defaultFetchPollResponseEventsTask.execute(params) + + // Then + fakeRoomAPI.verifyGetRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = null, + limit = FETCH_RELATED_EVENTS_LIMIT + ) + fakeRoomAPI.verifyGetRelations( + roomId = params.roomId, + eventId = params.startPollEventId, + relationType = RelationType.REFERENCE, + from = aNextBatchToken, + limit = FETCH_RELATED_EVENTS_LIMIT + ) + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") + // Check we save in DB the event2 which is a non stored poll response + verify { + event2.toEntity(aRoomId, SendState.SYNCED, any()) + eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + } + } + + private fun givenTaskParams(roomId: String, eventId: String) = FetchPollResponseEventsTask.Params( + roomId = roomId, + startPollEventId = eventId, + ) + + private fun givenARelationsResponse(events: List, nextBatch: String?): RelationsResponse { + return RelationsResponse( + chunks = events, + nextBatch = nextBatch, + prevBatch = null, + ) + } + + private fun givenAnEvent( + eventId: String, + isPollResponse: Boolean, + isEncrypted: Boolean, + ): Event { + val event = mockk(relaxed = true) + every { event.eventId } returns eventId + every { event.isPollResponse() } returns isPollResponse + every { event.isEncrypted() } returns isEncrypted + return event + } + + private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { + val eventEntities = existingIds.map { EventEntity(eventId = it) } + fakeMonarchy.givenWhere() + .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) + .givenFindAll(eventEntities) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt index 201423685ca..f3ab65f6c43 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt @@ -16,14 +16,17 @@ package org.matrix.android.sdk.internal.sync +import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.SyncConfig import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask -import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams +import org.matrix.android.sdk.internal.sync.filter.SyncFilterBuilder import org.matrix.android.sdk.test.fakes.FakeFilterRepository import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask @@ -31,7 +34,6 @@ import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask private const val A_FILTER_ID = "filter-id" private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities() private val A_SYNC_FILTER_PARAMS = SyncFilterParams( - lazyLoadMembersForMessageEvents = true, lazyLoadMembersForStateEvents = true, useThreadNotifications = true ) @@ -46,13 +48,16 @@ class DefaultGetCurrentFilterTaskTest { private val getCurrentFilterTask = DefaultGetCurrentFilterTask( filterRepository = filterRepository, homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance, - saveFilterTask = saveFilterTask + saveFilterTask = saveFilterTask, + matrixConfiguration = MatrixConfiguration( + applicationFlavor = "TestFlavor", + roomDisplayNameFallbackProvider = mockk(), + syncConfig = SyncConfig(syncFilterParams = SyncFilterParams(lazyLoadMembersForStateEvents = true, useThreadNotifications = true)), + ) ) @Test fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest { - filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) - homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) filterRepository.givenFilterStored(null, null) @@ -68,8 +73,6 @@ class DefaultGetCurrentFilterTaskTest { @Test fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest { - filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) - homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) @@ -82,8 +85,6 @@ class DefaultGetCurrentFilterTaskTest { @Test fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest { - filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS) - homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES) val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt new file mode 100644 index 00000000000..f2b62ad3ba3 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventDecryptor.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.crypto.EventDecryptor + +internal class FakeEventDecryptor { + val instance: EventDecryptor = mockk() + + fun givenDecryptEventAndSaveResultSuccess(event: Event) { + coJustRun { instance.decryptEventAndSaveResult(event, any()) } + } + + fun verifyDecryptEventAndSaveResult(event: Event, timeline: String) { + coVerify { instance.decryptEventAndSaveResult(event, timeline) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt similarity index 63% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt index 7347bee1657..cb75d8b7084 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFetchPollResponseEventsTask.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,9 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.session.sync +package org.matrix.android.sdk.test.fakes -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask -interface FilterService { - - /** - * Configure the filter for the sync. - */ - suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) -} +class FakeFetchPollResponseEventsTask : FetchPollResponseEventsTask by mockk(relaxed = true) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt index b8225f21d66..27a39120f8b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery import io.mockk.mockk import org.matrix.android.sdk.internal.session.filter.FilterRepository -import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams internal class FakeFilterRepository : FilterRepository by mockk() { @@ -27,8 +26,4 @@ internal class FakeFilterRepository : FilterRepository by mockk() { coEvery { getStoredSyncFilterId() } returns filterId coEvery { getStoredSyncFilterBody() } returns filterBody } - - fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) { - coEvery { getStoredFilterParams() } returns syncFilterParams - } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index afdcf111f89..ba124a86aa3 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -109,6 +109,14 @@ inline fun RealmQuery.givenLessThan( return this } +inline fun RealmQuery.givenIn( + fieldName: String, + values: List, +): RealmQuery { + every { `in`(fieldName, values.toTypedArray()) } returns this + return this +} + /** * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked. */ diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt new file mode 100644 index 00000000000..68dbbe7ea69 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRoomApi.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse + +internal class FakeRoomApi { + + val instance: RoomAPI = mockk() + + fun givenGetRelationsReturns( + from: String?, + relationsResponse: RelationsResponse, + ) { + coEvery { + instance.getRelations( + roomId = any(), + eventId = any(), + relationType = any(), + from = from, + limit = any() + ) + } returns relationsResponse + } + + fun verifyGetRelations( + roomId: String, + eventId: String, + relationType: String, + from: String?, + limit: Int, + ) { + coVerify { + instance.getRelations( + roomId = roomId, + eventId = eventId, + relationType = relationType, + from = from, + limit = limit + ) + } + } +} diff --git a/tools/emojis/emoji_picker_datasource_formatted.json b/tools/emojis/emoji_picker_datasource_formatted.json index c00bd10371e..0dcf9ccb251 100644 --- a/tools/emojis/emoji_picker_datasource_formatted.json +++ b/tools/emojis/emoji_picker_datasource_formatted.json @@ -3013,7 +3013,11 @@ "begging", "mercy", "puppy eyes", - "face" + "face", + "cry", + "tears", + "sad", + "grievance" ] }, "face-holding-back-tears": { @@ -3060,9 +3064,7 @@ "fearful", "scared", "terrified", - "nervous", - "oops", - "huh" + "nervous" ] }, "anxious-face-with-sweat": { diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 7d1073764d0..3bf77bf912f 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 14 +ext.versionPatch = 16 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt b/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt index 72137ed8e8b..68a54e99014 100644 --- a/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt +++ b/vector-app/src/androidTest/java/im/vector/app/EspressoExt.kt @@ -89,7 +89,7 @@ fun getString(@StringRes id: Int): String { return EspressoHelper.getCurrentActivity()!!.resources.getString(id) } -fun waitForView(viewMatcher: Matcher, timeout: Long = 10_000, waitForDisplayed: Boolean = true): ViewAction { +fun waitForView(viewMatcher: Matcher, timeout: Long = 20_000, waitForDisplayed: Boolean = true): ViewAction { return object : ViewAction { private val clock = DefaultClock() diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 52607bd9a1c..3439bcfcede 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -28,7 +28,6 @@ import im.vector.app.espresso.tools.ScreenshotFailureRule import im.vector.app.features.MainActivity import im.vector.app.getString import im.vector.app.ui.robot.ElementRobot -import im.vector.app.ui.robot.settings.labs.LabFeature import im.vector.app.ui.robot.settings.labs.LabFeaturesPreferences import im.vector.app.ui.robot.withDeveloperMode import org.junit.Rule @@ -133,6 +132,10 @@ class UiAllScreensSanityTest { } } + // Some instability with the bottomsheet + // not sure what's the source, maybe the expanded state? + Thread.sleep(10_000) + elementRobot.space { selectSpace(spaceName) } elementRobot.layoutPreferences { @@ -175,7 +178,6 @@ class UiAllScreensSanityTest { * Testing multiple threads screens */ private fun testThreadScreens() { - elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES) elementRobot.newRoom { createNewRoom { crawl() @@ -189,6 +191,5 @@ class UiAllScreensSanityTest { } } } - elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES) } } diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt index e5147c2085e..ad6d5e5df35 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceCreateRobot.kt @@ -28,7 +28,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn import im.vector.app.R import im.vector.app.espresso.tools.waitUntilActivityVisible -import im.vector.app.espresso.tools.waitUntilDialogVisible import im.vector.app.espresso.tools.waitUntilViewVisible import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.room.detail.RoomDetailActivity @@ -86,14 +85,17 @@ class SpaceCreateRobot { clickOn(R.id.nextButton) waitUntilViewVisible(withId(R.id.recyclerView)) clickOn(R.id.nextButton) +// waitUntilActivityVisible { +// waitUntilDialogVisible(withId(R.id.inviteByMxidButton)) +// } +// // close invite dialog +// pressBack() waitUntilActivityVisible { - waitUntilDialogVisible(withId(R.id.inviteByMxidButton)) + pressBack() } - // close invite dialog - pressBack() - waitUntilViewVisible(withId(R.id.timelineRecyclerView)) +// waitUntilViewVisible(withId(R.id.timelineRecyclerView)) // close room - pressBack() +// pressBack() waitUntilViewVisible(withId(R.id.roomListContainer)) } } diff --git a/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt b/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt index d04746bcd66..73a063857a4 100644 --- a/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt +++ b/vector-app/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt @@ -89,9 +89,8 @@ class SpaceMenuRobot { clickOnSheet(R.id.leaveSpace) waitUntilActivityVisible { waitUntilViewVisible(ViewMatchers.withId(R.id.roomList)) + clickOn(R.id.spaceLeaveSelectAll) + clickOn(R.id.spaceLeaveButton) } - clickOn(R.id.spaceLeaveSelectAll) - clickOn(R.id.spaceLeaveButton) - waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView)) } } diff --git a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt index dd04cb29869..a6d6fcd14bb 100644 --- a/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt +++ b/vector-app/src/main/java/im/vector/app/core/di/SingletonModule.kt @@ -70,11 +70,13 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.SyncConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.sync.filter.SyncFilterParams import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import javax.inject.Singleton @@ -157,6 +159,9 @@ import javax.inject.Singleton ), metricPlugins = vectorPlugins.plugins(), customEventTypesProvider = vectorCustomEventTypesProvider, + syncConfig = SyncConfig( + syncFilterParams = SyncFilterParams(lazyLoadMembersForStateEvents = true, useThreadNotifications = true) + ) ) } 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 b58d584dad5..d22ab51e7ae 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 @@ -46,6 +46,7 @@ import im.vector.app.features.home.UserColorAccountDataViewModel import im.vector.app.features.home.room.breadcrumbs.BreadcrumbsViewModel import im.vector.app.features.home.room.detail.TimelineViewModel import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel +import im.vector.app.features.home.room.detail.composer.link.SetLinkViewModel import im.vector.app.features.home.room.detail.search.SearchViewModel import im.vector.app.features.home.room.detail.timeline.action.MessageActionsViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryViewModel @@ -691,4 +692,9 @@ interface MavericksViewModelModule { fun vectorSettingsNotificationPreferenceViewModelFactory( factory: VectorSettingsNotificationPreferenceViewModel.Factory ): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SetLinkViewModel::class) + fun setLinkViewModelFactory(factory: SetLinkViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt new file mode 100644 index 00000000000..5a817b989e9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseDialogFragment.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2019 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.core.platform + +import android.content.Context +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.CallSuper +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.viewbinding.ViewBinding +import com.airbnb.mvrx.MavericksView +import dagger.hilt.android.EntryPointAccessors +import im.vector.app.R +import im.vector.app.core.di.ActivityEntryPoint +import im.vector.app.core.extensions.singletonEntryPoint +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.features.analytics.AnalyticsTracker +import im.vector.app.features.analytics.plan.MobileScreen +import im.vector.app.features.themes.ThemeUtils +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import reactivecircus.flowbinding.android.view.clicks +import timber.log.Timber + +/** + * Add Mavericks capabilities, handle DI and bindings. + */ +abstract class VectorBaseDialogFragment : DialogFragment(), MavericksView { + /* ========================================================================================== + * Analytics + * ========================================================================================== */ + + protected var analyticsScreenName: MobileScreen.ScreenName? = null + + protected lateinit var analyticsTracker: AnalyticsTracker + + /* ========================================================================================== + * View + * ========================================================================================== */ + + private var _binding: VB? = null + + // This property is only valid between onCreateView and onDestroyView. + protected val views: VB + get() = _binding!! + + abstract fun getBinding(inflater: LayoutInflater, container: ViewGroup?): VB + + /* ========================================================================================== + * View model + * ========================================================================================== */ + + private lateinit var viewModelFactory: ViewModelProvider.Factory + + protected val activityViewModelProvider + get() = ViewModelProvider(requireActivity(), viewModelFactory) + + protected val fragmentViewModelProvider + get() = ViewModelProvider(this, viewModelFactory) + + val vectorBaseActivity: VectorBaseActivity<*> by lazy { + activity as VectorBaseActivity<*> + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setStyle(STYLE_NORMAL, ThemeUtils.getApplicationThemeRes(requireContext())) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + _binding = getBinding(inflater, container) + return views.root + } + + @CallSuper + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } + + @CallSuper + override fun onDestroy() { + super.onDestroy() + } + + override fun onAttach(context: Context) { + val activityEntryPoint = EntryPointAccessors.fromActivity(vectorBaseActivity, ActivityEntryPoint::class.java) + viewModelFactory = activityEntryPoint.viewModelFactory() + val singletonEntryPoint = context.singletonEntryPoint() + analyticsTracker = singletonEntryPoint.analyticsTracker() + super.onAttach(context) + } + + override fun onResume() { + super.onResume() + Timber.i("onResume BottomSheet ${javaClass.simpleName}") + analyticsScreenName?.let { + analyticsTracker.screen(MobileScreen(screenName = it)) + } + } + + override fun onStart() { + super.onStart() + // This ensures that invalidate() is called for static screens that don't + // subscribe to a ViewModel. + postInvalidate() + requireDialog().window?.setWindowAnimations(R.style.Animation_AppCompat_Dialog) + } + + protected fun setArguments(args: Parcelable? = null) { + arguments = args.toMvRxBundle() + } + + /* ========================================================================================== + * Views + * ========================================================================================== */ + + protected fun View.debouncedClicks(onClicked: () -> Unit) { + clicks() + .onEach { onClicked() } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + /* ========================================================================================== + * ViewEvents + * ========================================================================================== */ + + protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .stream() + .onEach { + observer(it) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } +} diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index fbf89b76a48..c6a2635e6ce 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -25,7 +25,6 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase -import im.vector.app.features.sync.SyncUtils import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import timber.log.Timber @@ -43,9 +42,6 @@ class ConfigureAndStartSessionUseCase @Inject constructor( fun execute(session: Session, startSyncing: Boolean = true) { Timber.i("Configure and start session for ${session.myUserId}. startSyncing: $startSyncing") session.open() - session.coroutineScope.launch { - session.filterService().setSyncFilter(SyncUtils.getSyncFilterBuilder()) - } if (startSyncing) { session.startSyncing(context) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index faee8f652c6..9fe296e1f99 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -127,6 +127,7 @@ sealed class RoomDetailAction : VectorViewModelAction { object Pause : Recording() object Resume : Recording() object Stop : Recording() + object StopConfirmed : Recording() } sealed class Listening : VoiceBroadcastAction() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 399d5e0abe8..77dd826cfe6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -71,6 +71,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() + object DisplayPromptToStopVoiceBroadcast : RoomDetailViewEvents() + data class OpenStickerPicker(val widget: Widget) : RoomDetailViewEvents() object OpenIntegrationManager : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 6ab20275c25..980e8ebac55 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -413,6 +413,7 @@ class TimelineFragment : is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() + RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast -> displayPromptToStopVoiceBroadcast() } } @@ -2006,6 +2007,20 @@ class TimelineFragment : } } + private fun displayPromptToStopVoiceBroadcast() { + ConfirmationDialogBuilder + .show( + activity = requireActivity(), + askForReason = false, + confirmationRes = R.string.stop_voice_broadcast_content, + positiveRes = R.string.action_stop, + reasonHintRes = 0, + titleRes = R.string.stop_voice_broadcast_dialog_title + ) { + timelineViewModel.handle(RoomDetailAction.VoiceBroadcastAction.Recording.StopConfirmed) + } + } + override fun onTapToReturnToCall() { callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 02782783b84..ff24872ab83 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -633,7 +633,8 @@ class TimelineViewModel @AssistedInject constructor( } VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId) VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId) - VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) + VoiceBroadcastAction.Recording.Stop -> _viewEvents.post(RoomDetailViewEvents.DisplayPromptToStopVoiceBroadcast) + VoiceBroadcastAction.Recording.StopConfirmed -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) is VoiceBroadcastAction.Listening.PlayOrResume -> voiceBroadcastHelper.playOrResumePlayback(action.voiceBroadcast) VoiceBroadcastAction.Listening.Pause -> voiceBroadcastHelper.pausePlayback() VoiceBroadcastAction.Listening.Stop -> voiceBroadcastHelper.stopPlayback() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index d56ea8b7339..4849e20b6db 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -80,6 +80,9 @@ import im.vector.app.features.home.room.detail.AutoCompleter import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.TimelineViewModel +import im.vector.app.features.home.room.detail.composer.link.SetLinkFragment +import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedAction +import im.vector.app.features.home.room.detail.composer.link.SetLinkSharedActionViewModel import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet @@ -147,6 +150,7 @@ class MessageComposerFragment : VectorBaseFragment(), A private lateinit var sharedActionViewModel: MessageSharedActionViewModel private val attachmentViewModel: AttachmentTypeSelectorViewModel by fragmentViewModel() private val attachmentActionsViewModel: AttachmentTypeSelectorSharedActionViewModel by viewModels() + private val setLinkActionsViewModel: SetLinkSharedActionViewModel by viewModels() private val composer: MessageComposerView get() { return if (vectorPreferences.isRichTextEditorEnabled()) { @@ -212,6 +216,14 @@ class MessageComposerFragment : VectorBaseFragment(), A .onEach { onTypeSelected(it.attachmentType) } .launchIn(lifecycleScope) + setLinkActionsViewModel.stream() + .onEach { when (it) { + is SetLinkSharedAction.Insert -> views.richTextComposerLayout.insertLink(it.link, it.text) + is SetLinkSharedAction.Set -> views.richTextComposerLayout.setLink(it.link) + SetLinkSharedAction.Remove -> views.richTextComposerLayout.removeLink() + } } + .launchIn(lifecycleScope) + messageComposerViewModel.stateFlow.map { it.isFullScreen } .distinctUntilChanged() .onEach { isFullScreen -> @@ -385,6 +397,10 @@ class MessageComposerFragment : VectorBaseFragment(), A override fun onFullScreenModeChanged() = withState(messageComposerViewModel) { state -> messageComposerViewModel.handle(MessageComposerAction.SetFullScreen(!state.isFullScreen)) } + + override fun onSetLink(isTextSupported: Boolean, initialLink: String?) { + SetLinkFragment.show(isTextSupported, initialLink, childFragmentManager) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt index 44fcf22d4a8..b68f4046c8c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerView.kt @@ -45,4 +45,5 @@ interface Callback : ComposerEditText.Callback { fun onAddAttachment() fun onExpandOrCompactChange() fun onFullScreenModeChanged() + fun onSetLink(isTextSupported: Boolean, initialLink: String?) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index d69fe8edeb9..543210e006d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -49,6 +49,7 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.inputhandlers.models.InlineFormat +import io.element.android.wysiwyg.inputhandlers.models.LinkAction import io.element.android.wysiwyg.utils.RustErrorCollector import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction @@ -231,8 +232,25 @@ internal class RichTextComposerLayout @JvmOverloads constructor( addRichTextMenuItem(R.drawable.ic_composer_strikethrough, R.string.rich_text_editor_format_strikethrough, ComposerAction.STRIKE_THROUGH) { views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) } + addRichTextMenuItem(R.drawable.ic_composer_link, R.string.rich_text_editor_link, ComposerAction.LINK) { + views.richTextComposerEditText.getLinkAction()?.let { + when (it) { + LinkAction.InsertLink -> callback?.onSetLink(isTextSupported = true, initialLink = null) + is LinkAction.SetLink -> callback?.onSetLink(isTextSupported = false, initialLink = it.currentLink) + } + } + } } + fun setLink(link: String?) = + views.richTextComposerEditText.setLink(link) + + fun insertLink(link: String, text: String) = + views.richTextComposerEditText.insertLink(link, text) + + fun removeLink() = + views.richTextComposerEditText.removeLink() + @SuppressLint("ClickableViewAccessibility") private fun disallowParentInterceptTouchEvent(view: View) { view.setOnTouchListener { v, event -> diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt similarity index 52% rename from vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt index 9be59d31fdf..5cc31022ea0 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeFilterService.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkAction.kt @@ -14,23 +14,17 @@ * limitations under the License. */ -package im.vector.app.test.fakes +package im.vector.app.features.home.room.detail.composer.link -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.just -import io.mockk.mockk -import io.mockk.runs -import org.matrix.android.sdk.api.session.sync.FilterService -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder +import im.vector.app.core.platform.VectorViewModelAction -class FakeFilterService : FilterService by mockk() { +sealed class SetLinkAction : VectorViewModelAction { + data class LinkChanged( + val newLink: String + ) : SetLinkAction() - fun givenSetFilterSucceeds() { - coEvery { setSyncFilter(any()) } just runs - } - - fun verifySetSyncFilter(filterBuilder: SyncFilterBuilder) { - coVerify { setSyncFilter(filterBuilder) } - } + data class Save( + val link: String, + val text: String, + ) : SetLinkAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt new file mode 100644 index 00000000000..008a8017eee --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkFragment.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2021 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.room.detail.composer.link + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isGone +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseDialogFragment +import im.vector.app.databinding.FragmentSetLinkBinding +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize +import reactivecircus.flowbinding.android.widget.textChanges + +@AndroidEntryPoint +class SetLinkFragment : + VectorBaseDialogFragment() { + + @Parcelize + data class Args( + val isTextSupported: Boolean, + val initialLink: String?, + ) : Parcelable + + private val viewModel: SetLinkViewModel by fragmentViewModel() + private val sharedActionViewModel: SetLinkSharedActionViewModel by viewModels( + ownerProducer = { requireParentFragment() } + ) + private val args: Args by args() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSetLinkBinding { + return FragmentSetLinkBinding.inflate(inflater, container, false) + } + + companion object { + fun show(isTextSupported: Boolean, initialLink: String?, fragmentManager: FragmentManager) = + SetLinkFragment().apply { + setArguments(Args(isTextSupported, initialLink)) + }.show(fragmentManager, "SetLinkBottomSheet") + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.link.setText(args.initialLink) + views.link.textChanges() + .onEach { + viewModel.handle(SetLinkAction.LinkChanged(it.toString())) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + + views.save.debouncedClicks { + viewModel.handle( + SetLinkAction.Save( + link = views.link.text.toString(), + text = views.text.text.toString(), + ) + ) + } + + views.cancel.debouncedClicks(::onCancel) + views.remove.debouncedClicks(::onRemove) + + viewModel.observeViewEvents { + when (it) { + is SetLinkViewEvents.SavedLinkAndText -> handleInsert(link = it.link, text = it.text) + is SetLinkViewEvents.SavedLink -> handleSet(link = it.link) + } + } + + views.toolbar.setNavigationOnClickListener { + dismiss() + } + } + + override fun invalidate() = withState(viewModel) { viewState -> + views.toolbar.title = getString( + if (viewState.initialLink != null) { + R.string.set_link_edit + } else { + R.string.set_link_create + } + ) + + views.remove.isGone = !viewState.removeVisible + views.save.isEnabled = viewState.saveEnabled + views.textLayout.isGone = !viewState.isTextSupported + } + + private fun handleInsert(link: String, text: String) { + sharedActionViewModel.post(SetLinkSharedAction.Insert(text, link)) + dismiss() + } + + private fun handleSet(link: String) { + sharedActionViewModel.post(SetLinkSharedAction.Set(link)) + dismiss() + } + + private fun onRemove() { + sharedActionViewModel.post(SetLinkSharedAction.Remove) + dismiss() + } + + private fun onCancel() = dismiss() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt new file mode 100644 index 00000000000..fb9f3f0d5b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkSharedActionViewModel.kt @@ -0,0 +1,37 @@ +/* + * 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. + * + */ +package im.vector.app.features.home.room.detail.composer.link + +import im.vector.app.core.platform.VectorSharedAction +import im.vector.app.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class SetLinkSharedActionViewModel @Inject constructor() : + VectorSharedActionViewModel() + +sealed interface SetLinkSharedAction : VectorSharedAction { + data class Set( + val link: String, + ) : SetLinkSharedAction + + data class Insert( + val text: String, + val link: String, + ) : SetLinkSharedAction + + object Remove : SetLinkSharedAction +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt new file mode 100644 index 00000000000..cd42651c22c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewEvents.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 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.room.detail.composer.link + +import im.vector.app.core.platform.VectorViewEvents + +sealed class SetLinkViewEvents : VectorViewEvents { + + data class SavedLink( + val link: String, + ) : SetLinkViewEvents() + + data class SavedLinkAndText( + val link: String, + val text: String, + ) : SetLinkViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt new file mode 100644 index 00000000000..9a5b5cd8dd0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewModel.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 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.room.detail.composer.link + +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.VectorViewModel + +class SetLinkViewModel @AssistedInject constructor( + @Assisted private val initialState: SetLinkViewState, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SetLinkViewState): SetLinkViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: SetLinkAction) = when (action) { + is SetLinkAction.LinkChanged -> handleLinkChanged(action.newLink) + is SetLinkAction.Save -> handleSave(action.link, action.text) + } + + private fun handleLinkChanged(newLink: String) = setState { + copy(saveEnabled = newLink != initialLink.orEmpty()) + } + + private fun handleSave( + link: String, + text: String + ) = if (initialState.isTextSupported) { + _viewEvents.post(SetLinkViewEvents.SavedLinkAndText(link, text)) + } else { + _viewEvents.post(SetLinkViewEvents.SavedLink(link)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt new file mode 100644 index 00000000000..ea61f7eb72a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/link/SetLinkViewState.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 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.room.detail.composer.link + +import com.airbnb.mvrx.MavericksState + +data class SetLinkViewState( + val isTextSupported: Boolean, + val initialLink: String?, + val saveEnabled: Boolean, +) : MavericksState { + + constructor(args: SetLinkFragment.Args) : this( + isTextSupported = args.isTextSupported, + initialLink = args.initialLink, + saveEnabled = false, + ) + + val removeVisible = initialLink != null +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index 38fe1e8f177..b788d792142 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -93,7 +93,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem override fun renderLiveIndicator(holder: Holder) { when { voiceBroadcastState == null || voiceBroadcastState == VoiceBroadcastState.STOPPED -> renderNoLiveIndicator(holder) - voiceBroadcastState == VoiceBroadcastState.PAUSED || !player.isLiveListening -> renderPausedLiveIndicator(holder) + voiceBroadcastState == VoiceBroadcastState.PAUSED -> renderPausedLiveIndicator(holder) else -> renderPlayingLiveIndicator(holder) } } @@ -122,10 +122,14 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun bindSeekBar(holder: Holder) { with(holder) { - durationView.text = formatPlaybackTime(duration) + remainingTimeView.text = formatRemainingTime(duration) + elapsedTimeView.text = formatPlaybackTime(0) seekBar.max = duration seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) = Unit + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + remainingTimeView.text = formatRemainingTime(duration - progress) + elapsedTimeView.text = formatPlaybackTime(progress) + } override fun onStartTrackingTouch(seekBar: SeekBar) { isUserSeeking = true @@ -156,6 +160,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) + private fun formatRemainingTime(time: Int) = if (time < 1000) formatPlaybackTime(time) else String.format("-%s", formatPlaybackTime(time)) override fun unbind(holder: Holder) { super.unbind(holder) @@ -177,7 +182,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem val fastBackwardButton by bind(R.id.fastBackwardButton) val fastForwardButton by bind(R.id.fastForwardButton) val seekBar by bind(R.id.seekBar) - val durationView by bind(R.id.playbackDuration) + val remainingTimeView by bind(R.id.remainingTime) + val elapsedTimeView by bind(R.id.elapsedTime) val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) val listenersCountMetadata by bind(R.id.listenersCountMetadata) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt index 455f4778e87..e231686c276 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationBroadcastReceiver.kt @@ -118,6 +118,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleSmartReply(intent: Intent, context: Context) { val message = getReplyMessage(intent) val roomId = intent.getStringExtra(KEY_ROOM_ID) + val threadId = intent.getStringExtra(KEY_THREAD_ID) if (message.isNullOrBlank() || roomId.isNullOrBlank()) { // ignore this event @@ -126,13 +127,20 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { } activeSessionHolder.getActiveSession().let { session -> session.getRoom(roomId)?.let { room -> - sendMatrixEvent(message, session, room, context) + sendMatrixEvent(message, threadId, session, room, context) } } } - private fun sendMatrixEvent(message: String, session: Session, room: Room, context: Context?) { - room.sendService().sendTextMessage(message) + private fun sendMatrixEvent(message: String, threadId: String?, session: Session, room: Room, context: Context?) { + if (threadId != null) { + room.relationService().replyInThread( + rootThreadEventId = threadId, + replyInThreadText = message, + ) + } else { + room.sendService().sendTextMessage(message) + } // Create a new event to be displayed in the notification drawer, right now @@ -148,7 +156,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { body = message, imageUriString = null, roomId = room.roomId, - threadId = null, // needs to be changed: https://github.com/vector-im/element-android/issues/7475 + threadId = threadId, roomName = room.roomSummary()?.displayName ?: room.roomId, roomIsDirect = room.roomSummary()?.isDirect == true, outGoingMessage = true, @@ -223,6 +231,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { companion object { const val KEY_ROOM_ID = "roomID" + const val KEY_THREAD_ID = "threadID" const val KEY_TEXT_REPLY = "key_text_reply" } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 7bf78bdb950..5b3a2441373 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -657,7 +657,7 @@ class NotificationUtils @Inject constructor( // Quick reply if (!roomInfo.hasSmartReplyError) { - buildQuickReplyIntent(roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> + buildQuickReplyIntent(roomInfo.roomId, threadId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent -> val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY) .setLabel(stringProvider.getString(R.string.action_quick_reply)) .build() @@ -892,13 +892,17 @@ class NotificationUtils @Inject constructor( However, for Android devices running Marshmallow and below (API level 23 and below), it will be more appropriate to use an activity. Since you have to provide your own UI. */ - private fun buildQuickReplyIntent(roomId: String, senderName: String?): PendingIntent? { + private fun buildQuickReplyIntent(roomId: String, threadId: String?, senderName: String?): PendingIntent? { val intent: Intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { intent = Intent(context, NotificationBroadcastReceiver::class.java) intent.action = actionIds.smartReply intent.data = createIgnoredUri(roomId) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId) + threadId?.let { + intent.putExtra(NotificationBroadcastReceiver.KEY_THREAD_ID, it) + } + return PendingIntent.getBroadcast( context, clock.epochMillis().toInt(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index c21b044f1fb..15375ef679b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -223,7 +223,6 @@ class VectorSettingsDevicesFragment : override fun onViewAllClicked() { viewNavigator.navigateToOtherSessions( requireActivity(), - R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.UNVERIFIED, excludeCurrentDevice = true ) @@ -233,7 +232,6 @@ class VectorSettingsDevicesFragment : override fun onViewAllClicked() { viewNavigator.navigateToOtherSessions( requireActivity(), - R.string.device_manager_header_section_security_recommendations_title, DeviceManagerFilterType.INACTIVE, excludeCurrentDevice = true ) @@ -447,7 +445,6 @@ class VectorSettingsDevicesFragment : override fun onViewAllOtherSessionsClicked() { viewNavigator.navigateToOtherSessions( context = requireActivity(), - titleResourceId = R.string.device_manager_sessions_other_title, defaultFilter = DeviceManagerFilterType.ALL_SESSIONS, excludeCurrentDevice = true ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt index d4b3345fead..bcfa1c30db7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt @@ -31,12 +31,11 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() { fun navigateToOtherSessions( context: Context, - titleResourceId: Int, defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean, ) { context.startActivity( - OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice) + OtherSessionsActivity.newIntent(context, defaultFilter, excludeCurrentDevice) ) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt index ff6ce3faadd..f76c21da8e0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/details/SessionDetailsHeaderItem.kt @@ -27,7 +27,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.utils.DimensionConverter -private const val EXTRA_TOP_MARGIN_DP = 48 +private const val EXTRA_TOP_MARGIN_DP = 32 @EpoxyModelClass abstract class SessionDetailsHeaderItem : VectorEpoxyModel(R.layout.item_session_details_header) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt index 07202274add..2a43a9aade6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt @@ -53,6 +53,9 @@ class SecurityRecommendationView @JvmOverloads constructor( setImage(it) } + setOnClickListener { + callback?.onViewAllClicked() + } views.recommendationViewAllButton.setOnClickListener { callback?.onViewAllClicked() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index eecec72b0a2..5d2daf2941c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -75,7 +75,7 @@ class SessionInfoView @JvmOverloads constructor( renderDeviceLastSeenDetails( sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.deviceInfo, - sessionInfoViewState.isLastSeenDetailsVisible, + sessionInfoViewState.isLastActivityVisible, sessionInfoViewState.isShowingIpAddress, dateFormatter, drawableProvider, @@ -197,7 +197,7 @@ class SessionInfoView @JvmOverloads constructor( } else { views.sessionInfoLastActivityTextView.isGone = true } - views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isShowingIpAddress }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 5d3c4b4f4b6..6c7ca809ea7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -24,6 +24,6 @@ data class SessionInfoViewState( val isVerifyButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, - val isLastSeenDetailsVisible: Boolean = false, + val isLastActivityVisible: Boolean = false, val isShowingIpAddress: Boolean = false, ) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt index 22ca06eb1ef..502d9abca3e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.more +import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -42,6 +43,8 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment Unit)? = null + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding { return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false) } @@ -57,6 +60,11 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment super.invalidate() views.bottomSheetSessionLearnMoreTitle.text = viewState.title @@ -65,11 +73,12 @@ class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment updateLoading(state.isLoading) + updateFilterView(state.isSelectModeEnabled) if (state.devices is Success) { val devices = state.devices.invoke() renderDevices(devices, state.currentFilter, state.isShowingIpAddress) @@ -240,13 +243,17 @@ class OtherSessionsFragment : } } + private fun updateFilterView(isSelectModeEnabled: Boolean) { + views.otherSessionsFilterFrameLayout.isVisible = isSelectModeEnabled.not() + } + private fun updateToolbar(devices: List, isSelectModeEnabled: Boolean) { invalidateOptionsMenu() val title = if (isSelectModeEnabled) { val selection = devices.count { it.isSelected } stringProvider.getQuantityString(R.plurals.x_selected, selection, selection) } else { - getString(args.titleResourceId) + getString(R.string.device_manager_sessions_other_title) } toolbar?.title = title } @@ -341,6 +348,8 @@ class OtherSessionsFragment : override fun onOtherSessionLongClicked(deviceId: String) = withState(viewModel) { state -> if (!state.isSelectModeEnabled) { enableSelectMode(true, deviceId) + } else { + viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(deviceId)) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index f3df0cced08..399f99201b2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -224,7 +224,7 @@ class SessionOverviewFragment : isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted, isDetailsButtonVisible = false, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, - isLastSeenDetailsVisible = !isCurrentSession, + isLastActivityVisible = !isCurrentSession, isShowingIpAddress = viewState.isShowingIpAddress, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt index 2f671492e34..d2cbbbdee59 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver import androidx.core.widget.doOnTextChanged import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -62,12 +63,24 @@ class RenameSessionFragment : } private fun initEditText() { - views.renameSessionEditText.showKeyboard(andRequestFocus = true) + showKeyboard() views.renameSessionEditText.doOnTextChanged { text, _, _, _ -> viewModel.handle(RenameSessionAction.EditLocally(text.toString())) } } + private fun showKeyboard() { + val focusChangeListener = object : ViewTreeObserver.OnWindowFocusChangeListener { + override fun onWindowFocusChanged(hasFocus: Boolean) { + if (hasFocus) { + views.renameSessionEditText.showKeyboard(andRequestFocus = true) + } + views.renameSessionEditText.viewTreeObserver.removeOnWindowFocusChangeListener(this) + } + } + views.renameSessionEditText.viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener) + } + private fun initSaveButton() { views.renameSessionSave.debouncedClicks { viewModel.handle(RenameSessionAction.SaveModifications) @@ -89,7 +102,9 @@ class RenameSessionFragment : title = getString(R.string.device_manager_learn_more_session_rename_title), description = getString(R.string.device_manager_learn_more_session_rename), ) - SessionLearnMoreBottomSheet.show(childFragmentManager, args) + SessionLearnMoreBottomSheet + .show(childFragmentManager, args) + .onDismiss = { showKeyboard() } } private fun observeViewEvents() { diff --git a/vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt b/vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt deleted file mode 100644 index e3408d8814a..00000000000 --- a/vector/src/main/java/im/vector/app/features/sync/SyncUtils.kt +++ /dev/null @@ -1,48 +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. - */ - -package im.vector.app.features.sync - -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder - -object SyncUtils { - // Get only managed types by Element - private val listOfSupportedTimelineEventTypes = listOf( - // TODO Complete the list - EventType.MESSAGE - ) - - // Get only managed types by Element - private val listOfSupportedStateEventTypes = listOf( - // TODO Complete the list - EventType.STATE_ROOM_MEMBER - ) - - fun getSyncFilterBuilder(): SyncFilterBuilder { - return SyncFilterBuilder() - .useThreadNotifications(true) - .lazyLoadMembersForStateEvents(true) - /** - * Currently we don't set [lazy_load_members = true] for Filter.room.timeline even though we set it for RoomFilter which is used later to - * fetch messages in a room. It's not clear if it's done so by mistake or intentionally, so changing it could case side effects and need - * careful testing - * */ -// .lazyLoadMembersForMessageEvents(true) -// .listOfSupportedStateEventTypes(listOfSupportedStateEventTypes) -// .listOfSupportedTimelineEventTypes(listOfSupportedTimelineEventTypes) - } -} diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index b5c7b162d8c..3c902d162e3 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable import android.util.TypedValue import androidx.annotation.AttrRes import androidx.annotation.ColorInt +import androidx.annotation.StyleRes import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.graphics.drawable.DrawableCompat @@ -113,19 +114,16 @@ object ThemeUtils { */ fun setApplicationTheme(context: Context, aTheme: String) { currentTheme.set(aTheme) - context.setTheme( - when (aTheme) { - SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light - THEME_DARK_VALUE -> R.style.Theme_Vector_Dark - THEME_BLACK_VALUE -> R.style.Theme_Vector_Black - else -> R.style.Theme_Vector_Light - } - ) + context.setTheme(themeToRes(context, aTheme)) // Clear the cache mColorByAttr.clear() } + @StyleRes + fun getApplicationThemeRes(context: Context) = + themeToRes(context, currentTheme.get()) + /** * Set the activity theme according to the selected one. Default is Light, so if this is the current * theme, the theme is not changed. @@ -200,4 +198,13 @@ object ThemeUtils { DrawableCompat.setTint(tinted, color) return tinted } + + @StyleRes + private fun themeToRes(context: Context, theme: String): Int = + when (theme) { + SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.Theme_Vector_Dark else R.style.Theme_Vector_Light + THEME_DARK_VALUE -> R.style.Theme_Vector_Dark + THEME_BLACK_VALUE -> R.style.Theme_Vector_Black + else -> R.style.Theme_Vector_Light + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 1bc3078c8b4..d56f4ad7156 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -130,7 +130,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) - listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast && isLiveListening) + listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast) } override fun removeListener(voiceBroadcast: VoiceBroadcast, listener: Listener) { @@ -373,11 +373,6 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun onLiveListeningChanged(isLiveListening: Boolean) { - currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> - // Notify live mode change to all the listeners attached to the current voice broadcast id - listeners[voiceBroadcastId]?.forEach { listener -> listener.onLiveModeChanged(isLiveListening) } - } - // Live has ended and last chunk has been reached, we can stop the playback if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) { stop() diff --git a/vector/src/main/res/drawable/ic_composer_link.xml b/vector/src/main/res/drawable/ic_composer_link.xml new file mode 100644 index 00000000000..6d0f731ed97 --- /dev/null +++ b/vector/src/main/res/drawable/ic_composer_link.xml @@ -0,0 +1,12 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml index a7987e70b50..fd66aec1eac 100644 --- a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml +++ b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml @@ -2,9 +2,7 @@ + android:orientation="vertical"> - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingHorizontal="24dp" + android:paddingBottom="32dp" + android:scrollbarStyle="outsideOverlay"> - + android:paddingTop="24dp" + android:showDividers="none"> - + - + - + - + - + - + + + + + - + diff --git a/vector/src/main/res/layout/fragment_other_sessions.xml b/vector/src/main/res/layout/fragment_other_sessions.xml index e25b8b185f9..ce289bd125a 100644 --- a/vector/src/main/res/layout/fragment_other_sessions.xml +++ b/vector/src/main/res/layout/fragment_other_sessions.xml @@ -1,120 +1,132 @@ - - + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:layout_marginStart="72dp" + android:layout_marginTop="32dp" + android:layout_marginEnd="16dp" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + - + android:layout_marginTop="20dp" + android:gravity="start" + android:padding="0dp" + android:text="@string/device_manager_other_sessions_clear_filter" /> - + + + + + + + + + + + + + + + + + android:layout_gravity="end" + android:layout_marginEnd="8dp" + android:padding="8dp"> - + - + - + - + + - - - - - - - + -