diff --git a/.editorconfig b/.editorconfig index 231d35cfe4..4f23d46afd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -770,7 +770,7 @@ ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_assignment_wrap = off ij_kotlin_blank_lines_after_class_header = 0 diff --git a/changelog.d/6200.bugfix b/changelog.d/6200.bugfix new file mode 100644 index 0000000000..ee204b4567 --- /dev/null +++ b/changelog.d/6200.bugfix @@ -0,0 +1 @@ +Fixes room not being in space after upgrade diff --git a/changelog.d/6437.feature b/changelog.d/6437.feature new file mode 100644 index 0000000000..fb24819daf --- /dev/null +++ b/changelog.d/6437.feature @@ -0,0 +1 @@ +[Location sharing] - Delete action on a live message diff --git a/changelog.d/6487.feature b/changelog.d/6487.feature new file mode 100644 index 0000000000..3d58e80bd5 --- /dev/null +++ b/changelog.d/6487.feature @@ -0,0 +1 @@ +[Timeline] - Collapse redacted events diff --git a/changelog.d/6537.bugfix b/changelog.d/6537.bugfix new file mode 100644 index 0000000000..688fd5104c --- /dev/null +++ b/changelog.d/6537.bugfix @@ -0,0 +1 @@ +[Location Share] - Wrong room live location status bar visibility in timeline diff --git a/changelog.d/6546.feature b/changelog.d/6546.feature new file mode 100644 index 0000000000..988963b038 --- /dev/null +++ b/changelog.d/6546.feature @@ -0,0 +1 @@ +Updates FTUE registration to include username availability check and update copy diff --git a/changelog.d/6547.feature b/changelog.d/6547.feature new file mode 100644 index 0000000000..8086957a30 --- /dev/null +++ b/changelog.d/6547.feature @@ -0,0 +1 @@ +Updates the copy within the FTUE onboarding diff --git a/changelog.d/6567.feature b/changelog.d/6567.feature new file mode 100644 index 0000000000..9624c57a66 --- /dev/null +++ b/changelog.d/6567.feature @@ -0,0 +1 @@ +Share location with other apps diff --git a/changelog.d/6579.bugfix b/changelog.d/6579.bugfix new file mode 100644 index 0000000000..331115d5cc --- /dev/null +++ b/changelog.d/6579.bugfix @@ -0,0 +1 @@ +Do not log the live location of the user diff --git a/changelog.d/6584.misc b/changelog.d/6584.misc new file mode 100644 index 0000000000..1b53a8d755 --- /dev/null +++ b/changelog.d/6584.misc @@ -0,0 +1 @@ +Adds NewAppLayoutEnabled feature flag diff --git a/dependencies.gradle b/dependencies.gradle index 7435fe6e7f..97b4ad2ea3 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -13,7 +13,7 @@ ext.versions = [ def gradle = "7.1.3" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.6.21" -def kotlinCoroutines = "1.6.3" +def kotlinCoroutines = "1.6.4" def dagger = "2.42" def retrofit = "2.9.0" def arrow = "0.8.2" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt index 82f39806c0..bae4b06a05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -156,6 +156,20 @@ object MatrixPatterns { return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() } } + /** + * Extract user name from a matrix id. + * + * @param matrixId + * @return null if the input is not a valid matrixId + */ + fun extractUserNameFromId(matrixId: String): String? { + return if (isUserId(matrixId)) { + matrixId.removePrefix("@").substringBefore(":", missingDelimiterValue = "") + } else { + null + } + } + /** * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~), * or consist of more than 50 characters, are forbidden and the field should be ignored if received. 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 7f9ab4c6dd..59dc6c434d 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 @@ -202,7 +202,7 @@ data class Event( * It will return a decrypted text message or an empty string otherwise. */ fun getDecryptedTextSummary(): String? { - if (isRedacted()) return "Message Deleted" + if (isRedacted()) return "Message removed" val text = getDecryptedValue() ?: run { if (isPoll()) { return getPollQuestion() ?: "created a poll." @@ -371,6 +371,8 @@ fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClear fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER +fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO + fun Event.getRelationContent(): RelationDefaultContent? { return if (isEncrypted()) { content.toModel()?.relatesTo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 14095b67c0..cd8acbcccc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.room.location -import androidx.annotation.MainThread import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable @@ -59,16 +58,21 @@ interface LocationSharingService { */ suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult + /** + * Redact (delete) the live associated to the given beacon info event id. + * @param beaconInfoEventId event id of the initial beacon info state event + * @param reason Optional reason string + */ + suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) + /** * Returns a LiveData on the list of current running live location shares. */ - @MainThread fun getRunningLiveLocationShareSummaries(): LiveData> /** * Returns a LiveData on the live location share summary with the given eventId. * @param beaconInfoEventId event id of the initial beacon info state event */ - @MainThread fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt index 2e1668ebbb..8cfe3da031 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt @@ -33,5 +33,7 @@ enum class VersioningState { /** * The room has been upgraded, and the new room has been joined. */ - UPGRADED_ROOM_JOINED, + UPGRADED_ROOM_JOINED; + + fun isUpgraded() = this != NONE } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 9d8c8a13bd..d391abf1e6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -23,6 +23,7 @@ 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.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.isEdition +import org.matrix.android.sdk.api.session.events.model.isLiveLocation import org.matrix.android.sdk.api.session.events.model.isPoll import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.isSticker @@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean { return root.isSticker() } +fun TimelineEvent.isLiveLocation(): Boolean { + return root.isLiveLocation() +} + /** * Returns whether or not the event is a root thread event. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 56425cbc74..46ebbb7b71 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -37,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData * This class execute the registration request and is responsible to keep the session of interactive authentication. */ internal class DefaultRegistrationWizard( - authAPI: AuthAPI, - private val sessionCreator: SessionCreator, - private val pendingSessionStore: PendingSessionStore + authAPI: AuthAPI, + private val sessionCreator: SessionCreator, + private val pendingSessionStore: PendingSessionStore ) : RegistrationWizard { private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here") @@ -74,20 +74,20 @@ internal class DefaultRegistrationWizard( initialDeviceDisplayName: String? ): RegistrationResult { val params = RegistrationParams( - username = userName, - password = password, - initialDeviceDisplayName = initialDeviceDisplayName + username = userName, + password = password, + initialDeviceDisplayName = initialDeviceDisplayName ) return performRegistrationRequest(params, LoginType.PASSWORD) - .also { - pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) - .also { pendingSessionStore.savePendingSessionData(it) } - } + .also { + pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true) + .also { pendingSessionStore.savePendingSessionData(it) } + } } override suspend fun performReCaptcha(response: String): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response)) return performRegistrationRequest(params, LoginType.PASSWORD) @@ -95,7 +95,7 @@ internal class DefaultRegistrationWizard( override suspend fun acceptTerms(): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession)) return performRegistrationRequest(params, LoginType.PASSWORD) @@ -103,14 +103,14 @@ internal class DefaultRegistrationWizard( override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult { pendingSessionData = pendingSessionData.copy(currentThreePidData = null) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } return sendThreePid(threePid) } override suspend fun sendAgainThreePid(): RegistrationResult { val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") return sendThreePid(safeCurrentThreePid) } @@ -126,7 +126,7 @@ internal class DefaultRegistrationWizard( ) pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } val params = RegistrationParams( auth = if (threePid is RegisterThreePid.Email) { @@ -149,7 +149,7 @@ internal class DefaultRegistrationWizard( ) // Store data pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params)) - .also { pendingSessionStore.savePendingSessionData(it) } + .also { pendingSessionStore.savePendingSessionData(it) } // and send the sid a first time return performRegistrationRequest(params, LoginType.PASSWORD) @@ -157,7 +157,7 @@ internal class DefaultRegistrationWizard( override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult { val safeParam = pendingSessionData.currentThreePidData?.registrationParams - ?: throw IllegalStateException("developer error, no pending three pid") + ?: throw IllegalStateException("developer error, no pending three pid") return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis) } @@ -168,13 +168,13 @@ internal class DefaultRegistrationWizard( private suspend fun validateThreePid(code: String): RegistrationResult { val registrationParams = pendingSessionData.currentThreePidData?.registrationParams - ?: throw IllegalStateException("developer error, no pending three pid") + ?: throw IllegalStateException("developer error, no pending three pid") val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code") val validationBody = ValidationCodeBody( - clientSecret = pendingSessionData.clientSecret, - sid = safeCurrentData.addThreePidRegistrationResponse.sid, - code = code + clientSecret = pendingSessionData.clientSecret, + sid = safeCurrentData.addThreePidRegistrationResponse.sid, + code = code ) val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody)) if (validationResponse.isSuccess()) { @@ -189,7 +189,7 @@ internal class DefaultRegistrationWizard( override suspend fun dummy(): RegistrationResult { val safeSession = pendingSessionData.currentSession - ?: throw IllegalStateException("developer error, call createAccount() method first") + ?: throw IllegalStateException("developer error, call createAccount() method first") val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession)) return performRegistrationRequest(params, LoginType.PASSWORD) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index c4a6488258..0e511e14c2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -604,14 +604,16 @@ internal class MXOlmDevice @Inject constructor( * @param sharedHistory MSC3061, this key is sharable on invite * @return true if the operation succeeds. */ - fun addInboundGroupSession(sessionId: String, - sessionKey: String, - roomId: String, - senderKey: String, - forwardingCurve25519KeyChain: List, - keysClaimed: Map, - exportFormat: Boolean, - sharedHistory: Boolean): AddSessionResult { + fun addInboundGroupSession( + sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean, + sharedHistory: Boolean + ): AddSessionResult { val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") { if (exportFormat) { OlmInboundGroupSession.importSession(sessionKey) @@ -701,8 +703,8 @@ internal class MXOlmDevice @Inject constructor( val senderKey = megolmSessionData.senderKey ?: continue val roomId = megolmSessionData.roomId - val candidateSessionToImport = try { - MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) + val candidateSessionToImport = try { + MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true) } catch (e: Throwable) { Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId") continue diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt index 414416a0f6..38edbb7430 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt @@ -38,6 +38,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor( outgoingKeyRequestManager, cryptoStore, matrixConfiguration, - eventsManager) + eventsManager + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 48a25f2a8b..ceaee582c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -250,8 +250,10 @@ internal class MXMegolmEncryption( * @param sessionInfo the session info * @param devicesByUser the devices map */ - private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo, - devicesByUser: Map>) { + private suspend fun shareUserDevicesKey( + sessionInfo: MXOutboundSessionInfo, + devicesByUser: Map> + ) { val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also { Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index 10f59e6429..c36d572da6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -40,9 +40,9 @@ import javax.inject.Inject /** * Schema version history: - * 0, 1, 2: legacy Riot-Android - * 3: migrate to RiotX schema - * 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) + * 0, 1, 2: legacy Riot-Android; + * 3: migrate to RiotX schema; + * 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6). */ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, 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 b54aec26b2..9784412761 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 @@ -49,6 +49,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 32L, + schemaVersion = 33L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -99,5 +100,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 30) MigrateSessionTo030(realm).perform() if (oldVersion < 31) MigrateSessionTo031(realm).perform() if (oldVersion < 32) MigrateSessionTo032(realm).perform() + if (oldVersion < 33) MigrateSessionTo033(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt new file mode 100644 index 0000000000..0e3a8599c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt @@ -0,0 +1,33 @@ +/* + * 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.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary: adding new field relatedEventIds. + */ +internal class MigrateSessionTo033(realm: DynamicRealm) : RealmMigrator(realm, 33) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.addRealmListField(LiveLocationShareAggregatedSummaryEntityFields.RELATED_EVENT_IDS.`$`, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt index c5df8e9338..08ea06bb1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.database.model.livelocation +import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey @@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity( @PrimaryKey var eventId: String = "", + /** + * List of event ids used to compute the aggregated summary data. + */ + var relatedEventIds: RealmList = RealmList(), + var roomId: String = "", var userId: String = "", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index 6caa832110..1c19c21de2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -23,6 +23,11 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) +} + internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { return realm.where() .equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) @@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId) } + +internal fun EventAnnotationsSummaryEntity.Companion.get(realm: Realm, eventId: String): EventAnnotationsSummaryEntity? { + return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index d69f251f6f..fbf7e963a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -23,6 +23,14 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( + realm: Realm, + eventId: String, +): RealmQuery { + return realm.where() + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( realm: Realm, roomId: String, @@ -72,6 +80,13 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() } +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( + realm: Realm, + eventId: String, +): LiveLocationShareAggregatedSummaryEntity? { + return LiveLocationShareAggregatedSummaryEntity.where(realm, eventId).findFirst() +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser( realm: Realm, roomId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index f8a52f0b7e..b9f56cbc9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -88,6 +88,7 @@ import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationPro import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor +import org.matrix.android.sdk.internal.session.room.location.LiveLocationShareRedactionEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine @@ -321,6 +322,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor + @Binds + @IntoSet + abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor + @Binds @IntoSet abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor 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 e1dd22a211..d01324a35f 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 @@ -58,11 +58,13 @@ import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVi import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask +import org.matrix.android.sdk.internal.session.room.location.DefaultRedactLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask +import org.matrix.android.sdk.internal.session.room.location.RedactLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask @@ -339,4 +341,7 @@ internal abstract class RoomModule { @Binds abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask + + @Binds + abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 921749122b..3f5b1e1360 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import androidx.work.ExistingWorkPolicy import io.realm.Realm +import io.realm.RealmList import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -73,6 +74,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor( eventId = targetEventId ) + if (!isLive && !event.eventId.isNullOrEmpty()) { + // in this case, the received event is a new state event related to the previous one + addRelatedEventId(event.eventId, aggregatedSummary) + } + // remote event can stay with isLive == true while the local summary is no more active val isActive = aggregatedSummary.isActive.orTrue() && isLive val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } @@ -144,6 +150,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor( roomId = roomId, eventId = relatedEventId ) + + if (!event.eventId.isNullOrEmpty()) { + addRelatedEventId(event.eventId, aggregatedSummary) + } + val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0 val currentLocationTimestamp = ContentMapper .map(aggregatedSummary.lastLocationContent) @@ -160,6 +171,17 @@ internal class LiveLocationAggregationProcessor @Inject constructor( } } + private fun addRelatedEventId( + eventId: String, + aggregatedSummary: LiveLocationShareAggregatedSummaryEntity + ) { + Timber.d("adding related event id $eventId to summary of id ${aggregatedSummary.eventId}") + val updatedEventIds = aggregatedSummary.relatedEventIds.toMutableList().also { + it.add(eventId) + } + aggregatedSummary.relatedEventIds = RealmList(*updatedEventIds.toTypedArray()) + } + private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { LiveLocationShareAggregatedSummaryEntity .findActiveLiveInRoomForUser( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index a8a9691ce9..60312071d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( private val startLiveLocationShareTask: StartLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask, + private val redactLiveLocationShareTask: RedactLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, ) : LocationSharingService { @@ -102,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return stopLiveLocationShareTask.execute(params) } + override suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) { + val params = RedactLiveLocationShareTask.Params( + roomId = roomId, + beaconInfoEventId = beaconInfoEventId, + reason = reason + ) + return redactLiveLocationShareTask.execute(params) + } + override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt new file mode 100644 index 0000000000..fa3479ed3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt @@ -0,0 +1,65 @@ +/* + * 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.session.room.location + +import io.realm.Realm +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.LocalEcho +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +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.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import timber.log.Timber +import javax.inject.Inject + +/** + * Listens to the database for the insertion of any redaction event. + * Delete specifically the aggregated summary related to a redacted live location share event. + */ +internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO + } + + override suspend fun process(realm: Realm, event: Event) { + if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) { + return + } + + val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst() + ?: return + + if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) { + val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId) + + if (liveSummary != null) { + Timber.d("deleting live summary with id: ${liveSummary.eventId}") + liveSummary.deleteFromRealm() + val annotationsSummary = EventAnnotationsSummaryEntity.get(realm, eventId = redactedEvent.eventId) + if (annotationsSummary != null) { + Timber.d("deleting annotation summary with id: ${annotationsSummary.eventId}") + annotationsSummary.deleteFromRealm() + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt new file mode 100644 index 0000000000..ac855b81e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt @@ -0,0 +1,78 @@ +/* + * 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.location + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface RedactLiveLocationShareTask : Task { + data class Params( + val roomId: String, + val beaconInfoEventId: String, + val reason: String? + ) +} + +internal class DefaultRedactLiveLocationShareTask @Inject constructor( + @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val localEchoEventFactory: LocalEchoEventFactory, + private val eventSenderProcessor: EventSenderProcessor, +) : RedactLiveLocationShareTask { + + override suspend fun execute(params: RedactLiveLocationShareTask.Params) { + val relatedEventIds = getRelatedEventIdsOfLive(params.beaconInfoEventId) + Timber.d("beacon with id ${params.beaconInfoEventId} has related event ids: ${relatedEventIds.joinToString(", ")}") + + postRedactionWithLocalEcho( + eventId = params.beaconInfoEventId, + roomId = params.roomId, + reason = params.reason + ) + relatedEventIds.forEach { eventId -> + postRedactionWithLocalEcho( + eventId = eventId, + roomId = params.roomId, + reason = params.reason + ) + } + } + + private suspend fun getRelatedEventIdsOfLive(beaconInfoEventId: String): List { + return awaitTransaction(realmConfiguration) { realm -> + val aggregatedSummaryEntity = LiveLocationShareAggregatedSummaryEntity.get( + realm = realm, + eventId = beaconInfoEventId + ) + aggregatedSummaryEntity?.relatedEventIds?.toList() ?: emptyList() + } + } + + private fun postRedactionWithLocalEcho(eventId: String, roomId: String, reason: String?) { + Timber.d("posting redaction for event of id $eventId") + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, eventId, reason) + localEchoEventFactory.createLocalEcho(redactionEcho) + eventSenderProcessor.postRedaction(redactionEcho, reason) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index e33fbb56b1..cc86679cbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -74,6 +74,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE, + in EventType.STATE_ROOM_BEACON_INFO, + in EventType.BEACON_LOCATION_DATA, in EventType.POLL_START -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") val unsignedData = EventMapper.map(eventToPrune).unsignedData diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt index 323eee0b1c..7ed807d7cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt @@ -22,7 +22,7 @@ import timber.log.Timber /** * Throws in debug, only log in production. * As this method does not always throw, next statement should be a return. -*/ + */ internal fun fatalError(message: String) { if (BuildConfig.DEBUG) { error(message) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt index 0d0450adc2..ec587e0536 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt @@ -35,6 +35,23 @@ class MatrixPatternsTest { MatrixPatterns.isUserId(input) shouldBeEqualTo expected } } + + @Test + fun `given matrix id cases, when extracting userName, then returns expected`() { + val cases = listOf( + MatrixIdCase("foobar", userName = null), + MatrixIdCase("@foobar", userName = null), + MatrixIdCase("foobar@matrix.org", userName = null), + MatrixIdCase("@foobar: matrix.org", userName = null), + MatrixIdCase("foobar:matrix.org", userName = null), + MatrixIdCase("@foobar:matrix.org", userName = "foobar"), + ) + + cases.forEach { (input, expected) -> + MatrixPatterns.extractUserNameFromId(input) shouldBeEqualTo expected + } + } } private data class UserIdCase(val input: String, val isUserId: Boolean) +private data class MatrixIdCase(val input: String, val userName: String?) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index 933087af2b..a5e91714b7 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import androidx.work.ExistingWorkPolicy import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.UnsignedData @@ -199,9 +200,10 @@ internal class LiveLocationAggregationProcessorTest { age = 123, replacesState = AN_EVENT_ID ) + val stateEventId = "state-event-id" val event = Event( senderId = A_SENDER_ID, - eventId = "", + eventId = stateEventId, unsignedData = unsignedData ) val beaconInfo = MessageBeaconInfoContent( @@ -237,6 +239,7 @@ internal class LiveLocationAggregationProcessorTest { aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID aggregatedEntity.isActive shouldBeEqualTo false + aggregatedEntity.relatedEventIds shouldContain stateEventId aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS aggregatedEntity.lastLocationContent shouldBeEqualTo null previousEntities.forEach { entity -> @@ -324,7 +327,7 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP ) - givenLastSummaryQueryReturns( + val aggregatedEntity = givenLastSummaryQueryReturns( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, beaconLocationContent = lastBeaconLocationContent @@ -340,6 +343,7 @@ internal class LiveLocationAggregationProcessorTest { ) result shouldBeEqualTo false + aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID } @Test @@ -353,7 +357,7 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP - 60_000 ) - val entity = givenLastSummaryQueryReturns( + val aggregatedEntity = givenLastSummaryQueryReturns( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, beaconLocationContent = lastBeaconLocationContent @@ -369,7 +373,8 @@ internal class LiveLocationAggregationProcessorTest { ) result shouldBeEqualTo true - val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel() + aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID + val savedLocationData = ContentMapper.map(aggregatedEntity.lastLocationContent).toModel() savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index ef9bde2c49..a01f51604c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.slot import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -52,6 +54,7 @@ private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 private const val A_TIMEOUT = 15_000L private const val A_DESCRIPTION = "description" +private const val A_REASON = "reason" @ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { @@ -62,6 +65,7 @@ internal class DefaultLocationSharingServiceTest { private val startLiveLocationShareTask = mockk() private val stopLiveLocationShareTask = mockk() private val checkIfExistingActiveLiveTask = mockk() + private val redactLiveLocationShareTask = mockk() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk() private val defaultLocationSharingService = DefaultLocationSharingService( @@ -72,6 +76,7 @@ internal class DefaultLocationSharingServiceTest { startLiveLocationShareTask = startLiveLocationShareTask, stopLiveLocationShareTask = stopLiveLocationShareTask, checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask, + redactLiveLocationShareTask = redactLiveLocationShareTask, liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper ) @@ -209,6 +214,20 @@ internal class DefaultLocationSharingServiceTest { coVerify { stopLiveLocationShareTask.execute(expectedParams) } } + @Test + fun `live location share can be redacted`() = runTest { + coEvery { redactLiveLocationShareTask.execute(any()) } just runs + + defaultLocationSharingService.redactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + + val expectedParams = RedactLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + reason = A_REASON + ) + coVerify { redactLiveLocationShareTask.execute(expectedParams) } + } + @Test fun `livedata of live summaries is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt new file mode 100644 index 0000000000..b8618d1a79 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt @@ -0,0 +1,126 @@ +/* + * 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.location + +import io.mockk.unmockkAll +import io.realm.RealmList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor +import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" +private const val AN_EVENT_ID_1 = "event-id-1" +private const val AN_EVENT_ID_2 = "event-id-2" +private const val AN_EVENT_ID_3 = "event-id-3" +private const val A_REASON = "reason" + +@ExperimentalCoroutinesApi +class DefaultRedactLiveLocationShareTaskTest { + + private val fakeRealmConfiguration = FakeRealmConfiguration() + private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory() + private val fakeEventSenderProcessor = FakeEventSenderProcessor() + private val fakeRealm = FakeRealm() + + private val defaultRedactLiveLocationShareTask = DefaultRedactLiveLocationShareTask( + realmConfiguration = fakeRealmConfiguration.instance, + localEchoEventFactory = fakeLocalEchoEventFactory.instance, + eventSenderProcessor = fakeEventSenderProcessor + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given parameters when redacting then post redact events and related and creates redact local echos`() = runTest { + val params = createParams() + val relatedEventIds = listOf(AN_EVENT_ID_1, AN_EVENT_ID_2, AN_EVENT_ID_3) + val aggregatedSummaryEntity = createSummary(relatedEventIds) + givenSummaryForId(AN_EVENT_ID, aggregatedSummaryEntity) + fakeRealmConfiguration.givenAwaitTransaction>(fakeRealm.instance) + val redactEvents = givenCreateRedactEventWithLocalEcho(relatedEventIds + AN_EVENT_ID) + givenPostRedaction(redactEvents) + + defaultRedactLiveLocationShareTask.execute(params) + + verifyCreateRedactEventForEventIds(relatedEventIds + AN_EVENT_ID) + verifyCreateLocalEchoForEvents(redactEvents) + } + + private fun createParams() = RedactLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + reason = A_REASON + ) + + private fun createSummary(relatedEventIds: List): LiveLocationShareAggregatedSummaryEntity { + return LiveLocationShareAggregatedSummaryEntity( + eventId = AN_EVENT_ID, + relatedEventIds = RealmList(*relatedEventIds.toTypedArray()), + ) + } + + private fun givenSummaryForId(eventId: String, aggregatedSummaryEntity: LiveLocationShareAggregatedSummaryEntity) { + fakeRealm.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) + .givenFindFirst(aggregatedSummaryEntity) + } + + private fun givenCreateRedactEventWithLocalEcho(eventIds: List): List { + return eventIds.map { eventId -> + fakeLocalEchoEventFactory.givenCreateRedactEvent( + eventId = eventId, + withLocalEcho = true + ) + } + } + + private fun givenPostRedaction(redactEvents: List) { + redactEvents.forEach { + fakeEventSenderProcessor.givenPostRedaction(event = it, reason = A_REASON) + } + } + + private fun verifyCreateRedactEventForEventIds(eventIds: List) { + eventIds.forEach { eventId -> + fakeLocalEchoEventFactory.verifyCreateRedactEvent( + roomId = A_ROOM_ID, + eventId = eventId, + reason = A_REASON + ) + } + } + + private fun verifyCreateLocalEchoForEvents(events: List) { + events.forEach { redactionEvent -> + fakeLocalEchoEventFactory.verifyCreateLocalEcho(redactionEvent) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt new file mode 100644 index 0000000000..24d9c30039 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt @@ -0,0 +1,106 @@ +/* + * 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.location + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Test +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.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +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.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.givenDelete +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val AN_EVENT_ID = "event-id" +private const val A_REDACTED_EVENT_ID = "redacted-event-id" + +@ExperimentalCoroutinesApi +class LiveLocationShareRedactionEventProcessorTest { + + private val liveLocationShareRedactionEventProcessor = LiveLocationShareRedactionEventProcessor() + private val fakeRealm = FakeRealm() + + @Test + fun `given an event when checking if it should be processed then only event of type REDACTED is processed`() { + val eventId = AN_EVENT_ID + val eventType = EventType.REDACTION + val insertType = EventInsertType.INCREMENTAL_SYNC + + val result = liveLocationShareRedactionEventProcessor.shouldProcess( + eventId = eventId, + eventType = eventType, + insertType = insertType + ) + + result shouldBe true + } + + @Test + fun `given an event when checking if it should be processed then local echo is not processed`() { + val eventId = AN_EVENT_ID + val eventType = EventType.REDACTION + val insertType = EventInsertType.LOCAL_ECHO + + val result = liveLocationShareRedactionEventProcessor.shouldProcess( + eventId = eventId, + eventType = eventType, + insertType = insertType + ) + + result shouldBe false + } + + @Test + fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest { + val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID) + val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first()) + fakeRealm.givenWhere() + .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(redactedEventEntity) + val liveSummary = mockk() + every { liveSummary.eventId } returns A_REDACTED_EVENT_ID + liveSummary.givenDelete() + fakeRealm.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(liveSummary) + val annotationsSummary = mockk() + every { annotationsSummary.eventId } returns A_REDACTED_EVENT_ID + annotationsSummary.givenDelete() + fakeRealm.givenWhere() + .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(annotationsSummary) + + liveLocationShareRedactionEventProcessor.process(fakeRealm.instance, event = event) + + verify { + liveSummary.deleteFromRealm() + annotationsSummary.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt index fbdcf5bfd7..db04b8b8cb 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt @@ -27,4 +27,8 @@ internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() { fun givenPostEventReturns(event: Event, cancelable: Cancelable) { every { postEvent(event) } returns cancelable } + + fun givenPostRedaction(event: Event, reason: String?) { + every { postRedaction(event, reason) } returns mockk() + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt index 50ec85f14a..f484e32149 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt @@ -46,24 +46,6 @@ internal class FakeLocalEchoEventFactory { return event } - fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event { - val event = Event() - every { - instance.createLiveLocationEvent( - beaconInfoEventId = any(), - roomId = any(), - latitude = any(), - longitude = any(), - uncertainty = any() - ) - } returns event - - if (withLocalEcho) { - every { instance.createLocalEcho(event) } just runs - } - return event - } - fun verifyCreateStaticLocationEvent( roomId: String, latitude: Double, @@ -82,6 +64,24 @@ internal class FakeLocalEchoEventFactory { } } + fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createLiveLocationEvent( + beaconInfoEventId = any(), + roomId = any(), + latitude = any(), + longitude = any(), + uncertainty = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + fun verifyCreateLiveLocationEvent( roomId: String, beaconInfoEventId: String, @@ -100,6 +100,36 @@ internal class FakeLocalEchoEventFactory { } } + fun givenCreateRedactEvent(eventId: String, withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createRedactEvent( + roomId = any(), + eventId = eventId, + reason = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + + fun verifyCreateRedactEvent( + roomId: String, + eventId: String, + reason: String? + ) { + verify { + instance.createRedactEvent( + roomId = roomId, + eventId = eventId, + reason = reason + ) + } + } + fun verifyCreateLocalEcho(event: Event) { verify { instance.createLocalEcho(event) } } 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 0ebff87278..cb40889fb7 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 @@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes import io.mockk.MockKVerificationScope import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import io.mockk.verify import io.realm.Realm import io.realm.RealmModel +import io.realm.RealmObject import io.realm.RealmQuery import io.realm.RealmResults import io.realm.kotlin.where @@ -97,3 +100,10 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } 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. + */ +fun RealmObject.givenDelete() { + every { deleteFromRealm() } just runs +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt new file mode 100644 index 0000000000..15a9823c79 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -0,0 +1,41 @@ +/* + * 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.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.realm.Realm +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.database.awaitTransaction + +internal class FakeRealmConfiguration { + + init { + mockkStatic("org.matrix.android.sdk.internal.database.AsyncTransactionKt") + } + + val instance = mockk() + + fun givenAwaitTransaction(realm: Realm) { + val transaction = slot T>() + coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { + secondArg T>().invoke(realm) + } + } +} diff --git a/vector/build.gradle b/vector/build.gradle index 42f57099f7..aba53bf89c 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -280,10 +280,10 @@ android { } nightly { + initWith release applicationIdSuffix ".nightly" versionNameSuffix "-nightly" - initWith release // Just override the background color of the launcher icon for the nightly build. resValue "color", "launcher_background", "#07007E" diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt index 9533e93ed1..8fe65bd387 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugFeaturesStateFactory.kt @@ -80,6 +80,11 @@ class DebugFeaturesStateFactory @Inject constructor( key = DebugFeatureKeys.startDmOnFirstMsg, factory = VectorFeatures::shouldStartDmOnFirstMessage ), + createBooleanFeature( + label = "Enable New App Layout", + key = DebugFeatureKeys.newAppLayoutEnabled, + factory = VectorFeatures::isNewAppLayoutEnabled + ), ) ) } diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 1b178b5f48..23aad65653 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -72,6 +72,9 @@ class DebugVectorFeatures( override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg) ?: vectorFeatures.shouldStartDmOnFirstMessage() + override fun isNewAppLayoutEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled) + ?: vectorFeatures.isNewAppLayoutEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -131,4 +134,5 @@ object DebugFeatureKeys { val screenSharing = booleanPreferencesKey("screen-sharing") val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder") val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg") + val newAppLayoutEnabled = booleanPreferencesKey("new-app-layout-enabled") } diff --git a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt index 63d61af0e6..6bcbfe0ed5 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/BasicExtensions.kt @@ -19,6 +19,7 @@ package im.vector.app.core.extensions import android.util.Patterns import com.google.i18n.phonenumbers.NumberParseException import com.google.i18n.phonenumbers.PhoneNumberUtil +import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.extensions.ensurePrefix fun Boolean.toOnOff() = if (this) "ON" else "OFF" @@ -30,6 +31,8 @@ inline fun T.ooi(block: (T) -> Unit): T = also(block) */ fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches() +fun CharSequence.isMatrixId() = MatrixPatterns.isUserId(this.toString()) + /** * Return empty CharSequence if the CharSequence is null. */ diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt index d3ee765780..4739840f01 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextInputLayout.kt @@ -44,8 +44,15 @@ fun TextInputLayout.content() = editText().text.toString() fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty() fun TextInputLayout.clearErrorOnChange(lifecycleOwner: LifecycleOwner) { + onTextChange(lifecycleOwner) { + error = null + isErrorEnabled = false + } +} + +fun TextInputLayout.onTextChange(lifecycleOwner: LifecycleOwner, action: (CharSequence) -> Unit) { editText().textChanges() - .onEach { error = null } + .onEach(action) .launchIn(lifecycleOwner.lifecycleScope) } diff --git a/vector/src/main/java/im/vector/app/core/utils/SystemSettingsProvider.kt b/vector/src/main/java/im/vector/app/core/utils/SystemSettingsProvider.kt index 1a0b73fd21..14b67aaa09 100644 --- a/vector/src/main/java/im/vector/app/core/utils/SystemSettingsProvider.kt +++ b/vector/src/main/java/im/vector/app/core/utils/SystemSettingsProvider.kt @@ -36,6 +36,6 @@ class AndroidSystemSettingsProvider @Inject constructor( ) : SystemSettingsProvider { override fun getSystemFontScale(): Float { - return Settings.System.getFloat(context.contentResolver, Settings.System.FONT_SCALE) + return Settings.System.getFloat(context.contentResolver, Settings.System.FONT_SCALE, 1f) } } diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index eaacb0498e..b48fb62a3a 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -32,6 +32,7 @@ interface VectorFeatures { fun isScreenSharingEnabled(): Boolean fun forceUsageOfOpusEncoder(): Boolean fun shouldStartDmOnFirstMessage(): Boolean + fun isNewAppLayoutEnabled(): Boolean enum class OnboardingVariant { LEGACY, @@ -52,4 +53,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isScreenSharingEnabled(): Boolean = true override fun forceUsageOfOpusEncoder(): Boolean = false override fun shouldStartDmOnFirstMessage(): Boolean = false + override fun isNewAppLayoutEnabled(): Boolean = false } diff --git a/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt b/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt index e63aafbfc4..d71e52de8c 100644 --- a/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt +++ b/vector/src/main/java/im/vector/app/features/analytics/extensions/SignUpExt.kt @@ -21,11 +21,11 @@ import im.vector.app.features.onboarding.AuthenticationDescription fun AuthenticationDescription.AuthenticationType.toAnalyticsType() = when (this) { AuthenticationDescription.AuthenticationType.Password -> Signup.AuthenticationType.Password - AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple + AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple AuthenticationDescription.AuthenticationType.Facebook -> Signup.AuthenticationType.Facebook AuthenticationDescription.AuthenticationType.GitHub -> Signup.AuthenticationType.GitHub AuthenticationDescription.AuthenticationType.GitLab -> Signup.AuthenticationType.GitLab AuthenticationDescription.AuthenticationType.Google -> Signup.AuthenticationType.Google - AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO - AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other + AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO + AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 4b32f3307f..f4ffbb826a 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -70,7 +70,7 @@ class AvatarRenderer @Inject constructor( render( GlideApp.with(imageView), matrixItem, - DrawableImageViewTarget(imageView) + DrawableImageViewTarget(imageView), ) } @@ -103,7 +103,7 @@ class AvatarRenderer @Inject constructor( render( glideRequests, matrixItem, - DrawableImageViewTarget(imageView) + DrawableImageViewTarget(imageView), ) } @@ -123,7 +123,7 @@ class AvatarRenderer @Inject constructor( val matrixItem = MatrixItem.UserItem( // Need an id starting with @ id = "@${mappedContact.displayName}", - displayName = mappedContact.displayName + displayName = mappedContact.displayName, ) val placeholder = getPlaceholderDrawable(matrixItem) @@ -140,7 +140,7 @@ class AvatarRenderer @Inject constructor( val matrixItem = MatrixItem.UserItem( // Need an id starting with @ id = profileInfo.matrixId, - displayName = profileInfo.displayName + displayName = profileInfo.displayName, ) val placeholder = getPlaceholderDrawable(matrixItem) @@ -215,7 +215,7 @@ class AvatarRenderer @Inject constructor( .bold() .endConfig() .buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor) - .toBitmap(width = iconSize, height = iconSize) + .toBitmap(width = iconSize, height = iconSize), ) } } @@ -231,7 +231,7 @@ class AvatarRenderer @Inject constructor( addPlaceholder: Boolean ) { val transformations = mutableListOf>( - BlurTransformation(20, sampling) + BlurTransformation(20, sampling), ) if (colorFilter != null) { transformations.add(ColorFilterTransformation(colorFilter)) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index d82621977f..926c1eb113 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -219,7 +219,7 @@ class HomeActivity : is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId) is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId) HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK) - HomeActivitySharedAction.OnCloseSpace -> onCloseSpace() + HomeActivitySharedAction.OnCloseSpace -> onCloseSpace() } } .launchIn(lifecycleScope) 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 4d57647a1d..dcfee2d919 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 @@ -84,6 +84,4 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() - - data class ChangeLocationIndicator(val isVisible: Boolean) : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 3ec7166739..8500d1ed96 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -75,7 +75,8 @@ data class RoomDetailViewState( val switchToParentSpace: Boolean = false, val rootThreadEventId: String? = null, val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(), - val typingUsers: List? = null + val typingUsers: List? = null, + val isSharingLiveLocation: Boolean = false, ) : MavericksState { constructor(args: TimelineArgs) : this( 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 e86a7fe227..31c1004ef9 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 @@ -498,7 +498,6 @@ class TimelineFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() - is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it) } } @@ -663,10 +662,6 @@ class TimelineFragment @Inject constructor( ) } - private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) { - views.locationLiveStatusIndicator.isVisible = event.isVisible - } - private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) { if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable) } @@ -1686,6 +1681,11 @@ class TimelineFragment @Inject constructor( } else if (mainState.asyncInviter.complete) { vectorBaseActivity.finish() } + updateLiveLocationIndicator(mainState.isSharingLiveLocation) + } + + private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) { + views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation } private fun FragmentTimelineBinding.hideComposerViews() { @@ -1706,7 +1706,7 @@ class TimelineFragment @Inject constructor( private fun renderToolbar(roomSummary: RoomSummary?) { when { - isLocalRoom() -> { + isLocalRoom() -> { views.includeRoomToolbar.roomToolbarContentView.isVisible = false views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false setupToolbar(views.roomToolbar) @@ -1724,7 +1724,7 @@ class TimelineFragment @Inject constructor( } views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title) } - else -> { + else -> { views.includeRoomToolbar.roomToolbarContentView.isVisible = true views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false if (roomSummary == null) { 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 e305ccbec1..e3ea8a0826 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 @@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever @@ -105,6 +106,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService 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.isLiveLocation import org.matrix.android.sdk.api.session.sync.SyncRequestState import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState @@ -135,6 +137,7 @@ class TimelineViewModel @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val locationSharingServiceConnection: LocationSharingServiceConnection, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, + private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase, timelineFactory: TimelineFactory, appStateHandler: AppStateHandler, ) : VectorViewModel(initialState), @@ -770,7 +773,13 @@ class TimelineViewModel @AssistedInject constructor( private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { val event = room.getTimelineEvent(action.targetEventId) ?: return - room.sendService().redactEvent(event.root, action.reason) + if (event.isLiveLocation()) { + viewModelScope.launch { + redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason) + } + } else { + room.sendService().redactEvent(event.root, action.reason) + } } private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { @@ -1294,12 +1303,12 @@ class TimelineViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) } - override fun onLocationServiceRunning() { - _viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = true)) + override fun onLocationServiceRunning(roomIds: Set) { + setState { copy(isSharingLiveLocation = roomId in roomIds) } } override fun onLocationServiceStopped() { - _viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = false)) + setState { copy(isSharingLiveLocation = false) } // Bind again in case user decides to share live location without leaving the room locationSharingServiceConnection.bind(this) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt new file mode 100644 index 0000000000..ba91000b40 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt @@ -0,0 +1,30 @@ +/* + * 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.location + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.Room +import javax.inject.Inject + +class RedactLiveLocationShareEventUseCase @Inject constructor() { + + suspend fun execute(event: Event, room: Room, reason: String?) { + event.eventId + ?.takeUnless { it.isEmpty() } + ?.let { room.locationSharingService().redactLiveLocationShare(it, reason) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt new file mode 100644 index 0000000000..3bc3a5e351 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.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.timeline.action + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class CheckIfCanRedactEventUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder +) { + + fun execute(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { + // Only some event types are supported for the moment + val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) + + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + + return event.root.getClearType() in canRedactEventTypes && + // Message sent by the current user can always be redacted, else check permission for messages sent by other users + (event.root.senderId == activeSessionHolder.getActiveSession().myUserId || actionPermissions.canRedact) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 30786dc77a..3dfb6744e0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -82,6 +82,7 @@ class MessageActionsViewModel @AssistedInject constructor( private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val vectorPreferences: VectorPreferences, private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase, + private val checkIfCanRedactEventUseCase: CheckIfCanRedactEventUseCase, ) : VectorViewModel(initialState) { private val informationData = initialState.informationData @@ -518,12 +519,7 @@ class MessageActionsViewModel @AssistedInject constructor( } private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { - // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false - // Message sent by the current user can always be redacted - if (event.root.senderId == session.myUserId) return true - // Check permission for messages sent by other users - return actionPermissions.canRedact + return checkIfCanRedactEventUseCase.execute(event, actionPermissions) } private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 5f32696334..69b4f6e039 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -63,10 +63,10 @@ class EncryptionItemFactory @Inject constructor( isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> { R.string.direct_room_encryption_enabled_tile_description_future } - isDirect -> { + isDirect -> { R.string.direct_room_encryption_enabled_tile_description } - else -> { + else -> { R.string.encryption_enabled_tile_description } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt index 7f0fff8461..a4c906d97b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/LiveLocationShareMessageItemFactory.kt @@ -36,7 +36,6 @@ import im.vector.app.features.location.toLocationData import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.threeten.bp.LocalDateTime -import timber.log.Timber import javax.inject.Inject class LiveLocationShareMessageItemFactory @Inject constructor( @@ -135,7 +134,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor( liveLocationShareSummaryData.lastGeoUri.orEmpty(), getEndOfLiveDateTime(liveLocationShareSummaryData) ) - }.also { viewState -> Timber.d("computed viewState: $viewState") } + } } private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 800fc27e77..2149857ec2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper -import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem @@ -35,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue +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.EncryptionEventContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -53,6 +53,7 @@ class MergedHeaderItemFactory @Inject constructor( private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper ) { + private val mergeableEventTypes = listOf(EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_SERVER_ACL) private val collapsedEventIds = linkedSetOf() private val mergeItemCollapseStates = HashMap() @@ -78,19 +79,65 @@ class MergedHeaderItemFactory @Inject constructor( callback: TimelineEventController.Callback?, requestModelBuild: () -> Unit ): BasedMergedItem<*>? { - return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) { - // It's the first item before room.create - // Collapse all room configuration events - buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) - } else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { - null - } else { - buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + return when { + isStartOfRoomCreationSummary(event, nextEvent) -> + buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) -> + buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + isStartOfRedactedEventsSummary(event, items, currentPosition, addDaySeparator) -> + buildRedactedEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback) + else -> null } } - private fun buildMembershipEventsMergedSummary( + /** + * @param event the main timeline event + * @param nextEvent is an older event than event + */ + private fun isStartOfRoomCreationSummary( + event: TimelineEvent, + nextEvent: TimelineEvent?, + ): Boolean { + // It's the first item before room.create + // Collapse all room configuration events + return nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && + event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator) + } + + /** + * @param event the main timeline event + * @param nextEvent is an older event than event + * @param addDaySeparator true to add a day separator + */ + private fun isStartOfSameTypeEventsSummary( + event: TimelineEvent, + nextEvent: TimelineEvent?, + addDaySeparator: Boolean, + ): Boolean { + return event.root.getClearType() in mergeableEventTypes && + (nextEvent?.root?.getClearType() != event.root.getClearType() || addDaySeparator) + } + + /** + * @param event the main timeline event + * @param items all known items, sorted from newer event to oldest event + * @param currentPosition the current position + * @param addDaySeparator true to add a day separator + */ + private fun isStartOfRedactedEventsSummary( + event: TimelineEvent, + items: List, + currentPosition: Int, + addDaySeparator: Boolean, + ): Boolean { + val nextNonRedactionEvent = items + .subList(fromIndex = currentPosition + 1, toIndex = items.size) + .find { it.root.getClearType() != EventType.REDACTION } + return event.root.isRedacted() && + (!nextNonRedactionEvent?.root?.isRedacted().orFalse() || addDaySeparator) + } + + private fun buildSameTypeEventsMergedSummary( currentPosition: Int, items: List, partialState: TimelineEventController.PartialState, @@ -102,11 +149,42 @@ class MergedHeaderItemFactory @Inject constructor( val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents( items, currentPosition, - 2, + MIN_NUMBER_OF_MERGED_EVENTS, eventIdToHighlight, partialState.rootThreadEventId, partialState.isFromThreadTimeline() ) + return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback) + } + + private fun buildRedactedEventsMergedSummary( + currentPosition: Int, + items: List, + partialState: TimelineEventController.PartialState, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback? + ): MergedSimilarEventsItem_? { + val mergedEvents = timelineEventVisibilityHelper.prevRedactedEvents( + items, + currentPosition, + MIN_NUMBER_OF_MERGED_EVENTS, + eventIdToHighlight, + partialState.rootThreadEventId, + partialState.isFromThreadTimeline() + ) + return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback) + } + + private fun buildSimilarEventsMergedSummary( + mergedEvents: List, + partialState: TimelineEventController.PartialState, + event: TimelineEvent, + eventIdToHighlight: String?, + requestModelBuild: () -> Unit, + callback: TimelineEventController.Callback? + ): MergedSimilarEventsItem_? { return if (mergedEvents.isEmpty()) { null } else { @@ -127,7 +205,7 @@ class MergedHeaderItemFactory @Inject constructor( ) mergedData.add(data) } - val mergedEventIds = mergedEvents.map { it.localId } + val mergedEventIds = mergedEvents.map { it.localId }.toSet() // We try to find if one of the item id were used as mergeItemCollapseStates key // => handle case where paginating from mergeable events and we get more val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() @@ -140,12 +218,7 @@ class MergedHeaderItemFactory @Inject constructor( collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val summaryTitleResId = when (event.root.getClearType()) { - EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes - EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes - else -> null - } - summaryTitleResId?.let { summaryTitle -> + getSummaryTitleResId(event.root)?.let { summaryTitle -> val attributes = MergedSimilarEventsItem.Attributes( summaryTitleResId = summaryTitle, isCollapsed = isCollapsed, @@ -168,6 +241,16 @@ class MergedHeaderItemFactory @Inject constructor( } } + private fun getSummaryTitleResId(event: Event): Int? { + val type = event.getClearType() + return when { + type == EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes + type == EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes + event.isRedacted() -> R.plurals.room_removed_messages + else -> null + } + } + private fun buildRoomCreationMergedSummary( currentPosition: Int, items: List, @@ -191,7 +274,7 @@ class MergedHeaderItemFactory @Inject constructor( tmpPos-- prevEvent = items.getOrNull(tmpPos) } - return if (mergedEvents.size > 2) { + return if (mergedEvents.size > MIN_NUMBER_OF_MERGED_EVENTS) { var highlighted = false val mergedData = ArrayList(mergedEvents.size) mergedEvents.reversed() @@ -264,4 +347,8 @@ class MergedHeaderItemFactory @Inject constructor( fun isCollapsed(localId: Long): Boolean { return collapsedEventIds.contains(localId) } + + companion object { + private const val MIN_NUMBER_OF_MERGED_EVENTS = 2 + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 97ae3b634e..6c5a66d39d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -113,8 +113,14 @@ class TimelineItemFactory @Inject constructor( EventType.CALL_NEGOTIATE, EventType.REACTION, in EventType.POLL_RESPONSE, - in EventType.POLL_END, - in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params) + in EventType.POLL_END -> noticeItemFactory.create(params) + in EventType.BEACON_LOCATION_DATA -> { + if (event.root.isRedacted()) { + messageItemFactory.create(params) + } else { + noticeItemFactory.create(params) + } + } // Calls EventType.CALL_INVITE, EventType.CALL_HANGUP, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 85ffd7295c..23db2a721c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -51,12 +51,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_JOIN_RULES, EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, - ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO -} - -fun TimelineEvent.canBeMerged(): Boolean { - return root.getClearType() == EventType.STATE_ROOM_MEMBER || - root.getClearType() == EventType.STATE_ROOM_SERVER_ACL + ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA } fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 488aebde13..e6765bf35a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.extensions.localDateTime import im.vector.app.core.resources.UserPreferencesProvider +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.events.model.getRelationContent @@ -30,25 +31,38 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject -class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { +class TimelineEventVisibilityHelper @Inject constructor( + private val userPreferencesProvider: UserPreferencesProvider, +) { + + private interface PredicateToStopSearch { + /** + * Indicate whether a search on events should stop by comparing 2 given successive events. + * @param oldEvent the oldest event between the 2 events to compare + * @param newEvent the more recent event between the 2 events to compare + */ + fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean + } /** - * @param timelineEvents the events to search in + * @param timelineEvents the events to search in, sorted from oldest event to newer event * @param index the index to start computing (inclusive) * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param eventIdToHighlight used to compute visibility * @param rootThreadEventId the root thread event id if in a thread timeline * @param isFromThreadTimeline true if the timeline is a thread + * @param predicateToStop events are taken until this condition is met * - * @return a list of timeline events which have sequentially the same type following the next direction. + * @return a list of timeline events which meet sequentially the same criteria following the next direction. */ - private fun nextSameTypeEvents( + private fun nextEventsUntil( timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?, - isFromThreadTimeline: Boolean + isFromThreadTimeline: Boolean, + predicateToStop: PredicateToStopSearch ): List { if (index >= timelineEvents.size - 1) { return emptyList() @@ -65,13 +79,15 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else { nextSubList.subList(0, indexOfNextDay) } - val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() } - val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { + val indexOfFirstDifferentEvent = nextSameDayEvents.indexOfFirst { + predicateToStop.shouldStopSearch(oldEvent = timelineEvent.root, newEvent = it.root) + } + val similarEvents = if (indexOfFirstDifferentEvent == -1) { nextSameDayEvents } else { - nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) + nextSameDayEvents.subList(0, indexOfFirstDifferentEvent) } - val filteredSameTypeEvents = sameTypeEvents.filter { + val filteredSimilarEvents = similarEvents.filter { shouldShowEvent( timelineEvent = it, highlightedEventId = eventIdToHighlight, @@ -79,14 +95,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen rootThreadEventId = rootThreadEventId ) } - if (filteredSameTypeEvents.size < minSize) { - return emptyList() - } - return filteredSameTypeEvents + return if (filteredSimilarEvents.size < minSize) emptyList() else filteredSimilarEvents } /** - * @param timelineEvents the events to search in + * @param timelineEvents the events to search in, sorted from newer event to oldest event * @param index the index to start computing (inclusive) * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list * @param eventIdToHighlight used to compute visibility @@ -107,7 +120,44 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen return prevSub .reversed() .let { - nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline) + nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch { + override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean { + return oldEvent.getClearType() != newEvent.getClearType() + } + }) + } + } + + /** + * @param timelineEvents the events to search in, sorted from newer event to oldest event + * @param index the index to start computing (inclusive) + * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list + * @param eventIdToHighlight used to compute visibility + * @param rootThreadEventId the root thread eventId + * @param isFromThreadTimeline true if the timeline is a thread + * + * @return a list of timeline events which are all redacted following the prev direction. + */ + fun prevRedactedEvents( + timelineEvents: List, + index: Int, + minSize: Int, + eventIdToHighlight: String?, + rootThreadEventId: String?, + isFromThreadTimeline: Boolean + ): List { + val prevSub = timelineEvents + .subList(0, index + 1) + // Ensure to not take the REDACTION events into account + .filter { it.root.getClearType() != EventType.REDACTION } + return prevSub + .reversed() + .let { + nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch { + override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean { + return oldEvent.isRedacted() && !newEvent.isRedacted() + } + }) } } @@ -191,6 +241,10 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else root.eventId != rootThreadEventId } + if (root.getClearType() in EventType.BEACON_LOCATION_DATA) { + return !root.isRedacted() + } + return false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt index 2f91b9a35a..1f0404d659 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/upgrade/UpgradeRoomViewModelTask.kt @@ -84,8 +84,6 @@ class UpgradeRoomViewModelTask @Inject constructor( // autoJoin = currentInfo.autoJoin ?: false, suggested = currentInfo.suggested ) - - parentSpace.removeChildren(params.roomId) } } } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 44d61f9ed2..77f7c148d9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -146,17 +146,17 @@ class RoomListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() private val roomListSectionBuilder = RoomListSectionBuilder( - session, - stringProvider, - appStateHandler, - viewModelScope, - autoAcceptInvites, - { - updatableQuery = it - }, - suggestedRoomJoiningState, - !vectorPreferences.prefSpacesShowAllRoomInHome() - ) + session, + stringProvider, + appStateHandler, + viewModelScope, + autoAcceptInvites, + { + updatableQuery = it + }, + suggestedRoomJoiningState, + !vectorPreferences.prefSpacesShowAllRoomInHome() + ) val sections: List by lazy { roomListSectionBuilder.buildSections(initialState.displayMode) diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 412b28862a..725f23cddd 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -85,27 +85,27 @@ class EventHtmlRenderer @Inject constructor( } else { builder } - .usePlugin( - MarkwonInlineParserPlugin.create( - /* Configuring the Markwon inline formatting processor. - * Default settings are all Markdown features. Turn those off, only using the - * inline HTML processor and HTML entities processor. - */ - MarkwonInlineParser.factoryBuilderNoDefaults() - .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor - .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor + .usePlugin( + MarkwonInlineParserPlugin.create( + /* Configuring the Markwon inline formatting processor. + * Default settings are all Markdown features. Turn those off, only using the + * inline HTML processor and HTML entities processor. + */ + MarkwonInlineParser.factoryBuilderNoDefaults() + .addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor + .addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor + ) ) - ) - .usePlugin(object : AbstractMarkwonPlugin() { - override fun configureParser(builder: Parser.Builder) { - /* Configuring the Markwon block formatting processor. - * Default settings are all Markdown blocks. Turn those off. - */ - builder.enabledBlockTypes(kotlin.collections.emptySet()) - } - }) - .textSetter(PrecomputedFutureTextSetterCompat.create()) - .build() + .usePlugin(object : AbstractMarkwonPlugin() { + override fun configureParser(builder: Parser.Builder) { + /* Configuring the Markwon block formatting processor. + * Default settings are all Markdown blocks. Turn those off. + */ + builder.enabledBlockTypes(kotlin.collections.emptySet()) + } + }) + .textSetter(PrecomputedFutureTextSetterCompat.create()) + .build() val plugins: List = markwon.plugins diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt index 69ffc0e89e..dd18658059 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt @@ -26,9 +26,12 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn @@ -55,6 +58,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca @Inject lateinit var locationTracker: LocationTracker @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase + @Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase private val binder = LocalBinder() @@ -66,6 +70,9 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca private val jobs = mutableListOf() private var startInProgress = false + private val _roomIdsOfActiveLives = MutableSharedFlow>(replay = 1) + val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow() + override fun onCreate() { super.onCreate() Timber.i("onCreate") @@ -193,24 +200,30 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) { Timber.i("adding roomArgs for beaconEventId: $beaconEventId") roomArgsMap[beaconEventId] = roomArgs + launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) } } private fun removeRoomArgs(beaconEventId: String) { Timber.i("removing roomArgs for beaconEventId: $beaconEventId") roomArgsMap.remove(beaconEventId) + launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) } } private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) { launchWithActiveSession { session -> val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId) - .distinctUntilChangedBy { it.isActive } - .filter { it.isActive == false } + .distinctUntilChangedBy { it?.isActive } + .filter { it?.isActive == false || (it == null && isLiveRedacted(roomId, beaconEventId)) } .onEach { stopSharingLocation(beaconEventId) } .launchIn(session.coroutineScope) jobs.add(job) } } + private suspend fun isLiveRedacted(roomId: String, beaconEventId: String): Boolean { + return checkIfEventIsRedactedUseCase.execute(roomId = roomId, eventId = beaconEventId) + } + private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() @@ -220,6 +233,10 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca ) } + fun getRoomIdsOfActiveLives(): Set { + return roomArgsMap.map { it.value.roomId }.toSet() + } + override fun onBind(intent: Intent?): IBinder { return binder } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index 495b780188..3be73e9fd4 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -21,17 +21,22 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject import javax.inject.Singleton @Singleton class LocationSharingServiceConnection @Inject constructor( - private val context: Context -) : ServiceConnection, - LocationSharingAndroidService.Callback { + private val context: Context, + private val activeSessionHolder: ActiveSessionHolder +) : ServiceConnection, LocationSharingAndroidService.Callback { interface Callback { - fun onLocationServiceRunning() + fun onLocationServiceRunning(roomIds: Set) fun onLocationServiceStopped() fun onLocationServiceError(error: Throwable) } @@ -44,7 +49,7 @@ class LocationSharingServiceConnection @Inject constructor( addCallback(callback) if (isBound) { - callback.onLocationServiceRunning() + callback.onLocationServiceRunning(getRoomIdsOfActiveLives()) } else { Intent(context, LocationSharingAndroidService::class.java).also { intent -> context.bindService(intent, this, 0) @@ -56,12 +61,24 @@ class LocationSharingServiceConnection @Inject constructor( removeCallback(callback) } + private fun getRoomIdsOfActiveLives(): Set { + return locationSharingAndroidService?.getRoomIdsOfActiveLives() ?: emptySet() + } + override fun onServiceConnected(className: ComponentName, binder: IBinder) { - locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { - it.callback = this + locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { service -> + service.callback = this + getActiveSessionCoroutineScope()?.let { scope -> + service.roomIdsOfActiveLives + .onEach(::onRoomIdsUpdate) + .launchIn(scope) + } } isBound = true - onCallbackActionNoArg(Callback::onLocationServiceRunning) + } + + private fun getActiveSessionCoroutineScope(): CoroutineScope? { + return activeSessionHolder.getSafeActiveSession()?.coroutineScope } override fun onServiceDisconnected(className: ComponentName) { @@ -71,6 +88,10 @@ class LocationSharingServiceConnection @Inject constructor( onCallbackActionNoArg(Callback::onLocationServiceStopped) } + private fun onRoomIdsUpdate(roomIds: Set) { + forwardRoomIdsToCallbacks(roomIds) + } + override fun onServiceError(error: Throwable) { forwardErrorToCallbacks(error) } @@ -87,6 +108,10 @@ class LocationSharingServiceConnection @Inject constructor( callbacks.toList().forEach(action) } + private fun forwardRoomIdsToCallbacks(roomIds: Set) { + callbacks.toList().forEach { it.onLocationServiceRunning(roomIds) } + } + private fun forwardErrorToCallbacks(error: Throwable) { callbacks.toList().forEach { it.onLocationServiceError(error) } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt index 0d8b70ccda..bc38889d7f 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.location.live import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom @@ -31,13 +31,13 @@ class GetLiveLocationShareSummaryUseCase @Inject constructor( private val session: Session, ) { - suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { + suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { Timber.d("getting flow for roomId=$roomId and eventId=$eventId") session.getRoom(roomId) ?.locationSharingService() ?.getLiveLocationShareSummary(eventId) ?.asFlow() - ?.mapNotNull { it.getOrNull() } + ?.map { it.getOrNull() } ?: emptyFlow() } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt b/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt index cf360ec277..be8e774ad0 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/LiveLocationLabsFlagPromotionBottomSheet.kt @@ -29,7 +29,7 @@ import im.vector.app.databinding.BottomSheetLiveLocationLabsFlagPromotionBinding * This should not be shown if the user already enabled the labs flag. */ class LiveLocationLabsFlagPromotionBottomSheet : - VectorBaseBottomSheetDialogFragment() { + VectorBaseBottomSheetDialogFragment() { override val showExpanded = true diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapMarkerOptionsDialog.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapMarkerOptionsDialog.kt new file mode 100644 index 0000000000..5f9d4a4db5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapMarkerOptionsDialog.kt @@ -0,0 +1,59 @@ +/* + * 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.location.live.map + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import im.vector.app.R +import im.vector.app.databinding.ViewLiveLocationMarkerPopupBinding + +class LocationLiveMapMarkerOptionsDialog( + context: Context, +) : PopupWindow() { + + interface Callback { + fun onShareLocationClicked() + } + + private val views: ViewLiveLocationMarkerPopupBinding + + var callback: Callback? = null + + init { + contentView = View.inflate(context, R.layout.view_live_location_marker_popup, null) + + views = ViewLiveLocationMarkerPopupBinding.bind(contentView) + + width = ViewGroup.LayoutParams.WRAP_CONTENT + height = ViewGroup.LayoutParams.WRAP_CONTENT + inputMethodMode = INPUT_METHOD_NOT_NEEDED + isFocusable = true + isTouchable = true + + contentView.setOnClickListener { + callback?.onShareLocationClicked() + } + } + + fun show(anchorView: View) { + contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + // By default the left side of the dialog is aligned with the pin. We need shift it to the left to make it's center aligned with the pin. + showAsDropDown(anchorView, -contentView.measuredWidth / 2, 0) + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt index a57ba74685..1d6afa9cda 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewFragment.kt @@ -33,6 +33,7 @@ import com.mapbox.mapboxsdk.maps.MapboxMap import com.mapbox.mapboxsdk.maps.MapboxMapOptions import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.maps.SupportMapFragment +import com.mapbox.mapboxsdk.plugins.annotation.Symbol import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property @@ -42,6 +43,7 @@ import im.vector.app.core.extensions.addChildFragment import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.DimensionConverter +import im.vector.app.core.utils.openLocation import im.vector.app.databinding.FragmentLocationLiveMapViewBinding import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.zoomToBounds @@ -120,6 +122,10 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment) { // NOOP } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt b/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt index 3672b8eef3..c540871ab7 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/AuthenticationDescription.kt @@ -41,12 +41,12 @@ sealed interface AuthenticationDescription : Parcelable { } fun SsoIdentityProvider?.toAuthenticationType() = when (this?.brand) { - SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google - SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub - SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple + SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google + SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub + SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple SsoIdentityProvider.BRAND_FACEBOOK -> AuthenticationType.Facebook - SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab - SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO - null -> AuthenticationType.SSO - else -> AuthenticationType.SSO + SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab + SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO + null -> AuthenticationType.SSO + else -> AuthenticationType.SSO } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index 96b0bc45d6..d07ac46b85 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -52,9 +52,13 @@ sealed interface OnboardingAction : VectorViewModelAction { object ResendResetPassword : OnboardingAction object ResetPasswordMailConfirmed : OnboardingAction - data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction + sealed interface UserNameEnteredAction : OnboardingAction { + data class Registration(val userId: String) : UserNameEnteredAction + data class Login(val userId: String) : UserNameEnteredAction + } sealed interface AuthenticateAction : OnboardingAction { data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction + data class RegisterWithMatrixId(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction } @@ -71,6 +75,7 @@ sealed interface OnboardingAction : VectorViewModelAction { object ResetSignMode : ResetAction object ResetAuthenticationAttempt : ResetAction object ResetResetPassword : ResetAction + object ResetSelectedRegistrationUserName : ResetAction // Homeserver history object ClearHomeServerHistory : OnboardingAction diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 27c501176e..52c32d88e4 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -28,6 +28,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.extensions.cancelCurrentOnSet import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.extensions.inferNoConnectivity +import im.vector.app.core.extensions.isMatrixId +import im.vector.app.core.extensions.toReducedUrl import im.vector.app.core.extensions.vectorStore import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.BuildMeta @@ -57,6 +59,7 @@ import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.auth.login.LoginWizard +import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.failure.isHomeserverUnavailable import org.matrix.android.sdk.api.session.Session @@ -144,7 +147,7 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action) is OnboardingAction.InitWith -> handleInitWith(action) is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) } - is OnboardingAction.MaybeUpdateHomeserverFromMatrixId -> handleMaybeUpdateHomeserver(action) + is OnboardingAction.UserNameEnteredAction -> handleUserNameEntered(action) is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) } is OnboardingAction.LoginWithToken -> handleLoginWithToken(action) is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action) @@ -167,13 +170,47 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleMaybeUpdateHomeserver(action: OnboardingAction.MaybeUpdateHomeserverFromMatrixId) { - val isFullMatrixId = MatrixPatterns.isUserId(action.userId) + private fun handleUserNameEntered(action: OnboardingAction.UserNameEnteredAction) { + when (action) { + is OnboardingAction.UserNameEnteredAction.Login -> maybeUpdateHomeserver(action.userId) + is OnboardingAction.UserNameEnteredAction.Registration -> maybeUpdateHomeserver(action.userId, continuation = { userName -> + checkUserNameAvailability(userName) + }) + } + } + + private fun maybeUpdateHomeserver(userNameOrMatrixId: String, continuation: suspend (String) -> Unit = {}) { + val isFullMatrixId = MatrixPatterns.isUserId(userNameOrMatrixId) if (isFullMatrixId) { - val domain = action.userId.getServerName().substringBeforeLast(":").ensureProtocol() - handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain)) + val domain = userNameOrMatrixId.getServerName().substringBeforeLast(":").ensureProtocol() + handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain), postAction = { + val userName = MatrixPatterns.extractUserNameFromId(userNameOrMatrixId) ?: throw IllegalStateException("unexpected non matrix id") + continuation(userName) + }) } else { - // ignore the action + currentJob = viewModelScope.launch { continuation(userNameOrMatrixId) } + } + } + + private suspend fun checkUserNameAvailability(userName: String) { + when (val result = registrationWizard.registrationAvailable(userName)) { + RegistrationAvailability.Available -> { + setState { + copy( + registrationState = RegistrationState( + isUserNameAvailable = true, + selectedMatrixId = when { + userName.isMatrixId() -> userName + else -> "@$userName:${selectedHomeserver.userFacingUrl.toReducedUrl()}" + }, + ) + ) + } + } + + is RegistrationAvailability.NotAvailable -> { + _viewEvents.post(OnboardingViewEvents.Failure(result.failure)) + } } } @@ -184,7 +221,12 @@ class OnboardingViewModel @AssistedInject constructor( private fun handleAuthenticateAction(action: AuthenticateAction) { when (action) { - is AuthenticateAction.Register -> handleRegisterWith(action) + is AuthenticateAction.Register -> handleRegisterWith(action.username, action.password, action.initialDeviceName) + is AuthenticateAction.RegisterWithMatrixId -> handleRegisterWith( + MatrixPatterns.extractUserNameFromId(action.matrixId) ?: throw IllegalStateException("unexpected non matrix id"), + action.password, + action.initialDeviceName + ) is AuthenticateAction.Login -> handleLogin(action) is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null) } @@ -322,17 +364,17 @@ class OnboardingViewModel @AssistedInject constructor( ) } - private fun handleRegisterWith(action: AuthenticateAction.Register) { + private fun handleRegisterWith(userName: String, password: String, initialDeviceName: String) { setState { val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password) copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription)) } - reAuthHelper.data = action.password + reAuthHelper.data = password handleRegisterAction( RegisterAction.CreateAccount( - action.username, - action.password, - action.initialDeviceName + userName, + password, + initialDeviceName ) ) } @@ -368,7 +410,12 @@ class OnboardingViewModel @AssistedInject constructor( OnboardingAction.ResetAuthenticationAttempt -> { viewModelScope.launch { authenticationService.cancelPendingLoginOrRegistration() - setState { copy(isLoading = false) } + setState { + copy( + isLoading = false, + registrationState = RegistrationState(), + ) + } } } OnboardingAction.ResetResetPassword -> { @@ -380,6 +427,11 @@ class OnboardingViewModel @AssistedInject constructor( } } OnboardingAction.ResetDeeplinkConfig -> loginConfig = null + OnboardingAction.ResetSelectedRegistrationUserName -> { + setState { + copy(registrationState = RegistrationState()) + } + } } } @@ -511,7 +563,7 @@ class OnboardingViewModel @AssistedInject constructor( setState { copy(isLoading = false, resetState = ResetState()) } val nextEvent = when { vectorFeatures.isOnboardingCombinedLoginEnabled() -> OnboardingViewEvents.OnResetPasswordComplete - else -> OnboardingViewEvents.OpenResetPasswordComplete + else -> OnboardingViewEvents.OpenResetPasswordComplete } _viewEvents.post(nextEvent) }, @@ -593,6 +645,7 @@ class OnboardingViewModel @AssistedInject constructor( val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities() val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull() state.personalizationState.copy( + displayName = state.registrationState.selectedMatrixId?.let { MatrixPatterns.extractUserNameFromId(it) }, supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName, supportsChangingProfilePicture = capabilityOverrides?.canChangeAvatar ?: homeServerCapabilities.canChangeAvatar ) @@ -619,27 +672,31 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null) { + private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null, postAction: suspend () -> Unit = {}) { val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl) if (homeServerConnectionConfig == null) { // This is invalid _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { - startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride) + startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) } } private fun startAuthenticationFlow( trigger: OnboardingAction.HomeServerChange, homeServerConnectionConfig: HomeServerConnectionConfig, - serverTypeOverride: ServerType? + serverTypeOverride: ServerType?, + postAction: suspend () -> Unit = {}, ) { currentHomeServerConnectionConfig = homeServerConnectionConfig currentJob = viewModelScope.launch { setState { copy(isLoading = true) } runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold( - onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) }, + onSuccess = { + onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) + postAction() + }, onFailure = { onAuthenticationStartError(it, trigger) } ) setState { copy(isLoading = false) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index c072f13bca..fe2134618d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -48,6 +48,9 @@ data class OnboardingViewState( val knownCustomHomeServersUrls: List = emptyList(), val isForceLoginFallbackEnabled: Boolean = false, + @PersistState + val registrationState: RegistrationState = RegistrationState(), + @PersistState val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(), @@ -66,7 +69,6 @@ enum class OnboardingFlow { @Parcelize data class SelectedHomeserverState( - val description: String? = null, val userFacingUrl: String? = null, val upstreamUrl: String? = null, val preferredLoginMode: LoginMode = LoginMode.Unknown, @@ -96,3 +98,9 @@ data class ResetState( data class SelectedAuthenticationState( val description: AuthenticationDescription? = null, ) : Parcelable + +@Parcelize +data class RegistrationState( + val isUserNameAvailable: Boolean = false, + val selectedMatrixId: String? = null, +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt index 7b6205bfce..2dc9a05154 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/StartAuthenticationFlowUseCase.kt @@ -16,10 +16,7 @@ package im.vector.app.features.onboarding -import im.vector.app.R import im.vector.app.core.extensions.containsAllItems -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.ensureTrailingSlash import im.vector.app.features.login.LoginMode import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig @@ -29,7 +26,6 @@ import javax.inject.Inject class StartAuthenticationFlowUseCase @Inject constructor( private val authenticationService: AuthenticationService, - private val stringProvider: StringProvider ) { suspend fun execute(config: HomeServerConnectionConfig): StartAuthenticationResult { @@ -46,10 +42,6 @@ class StartAuthenticationFlowUseCase @Inject constructor( config: HomeServerConnectionConfig, preferredLoginMode: LoginMode ) = SelectedHomeserverState( - description = when (config.homeServerUri.toString()) { - matrixOrgUrl() -> stringProvider.getString(R.string.ftue_auth_create_account_matrix_dot_org_server_description) - else -> null - }, userFacingUrl = config.homeServerUri.toString(), upstreamUrl = authFlow.homeServerUrl, preferredLoginMode = preferredLoginMode, @@ -57,8 +49,6 @@ class StartAuthenticationFlowUseCase @Inject constructor( isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported ) - private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() - private fun LoginFlowResult.findPreferredLoginMode() = when { supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders) supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders) diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt index ded9cb00fa..9f2aadef5c 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt @@ -16,15 +16,18 @@ package im.vector.app.features.onboarding.ftueauth +import android.graphics.Typeface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.text.toSpannable import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.animations.play import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.utils.isAnimationEnabled +import im.vector.app.core.utils.styleMatchingText import im.vector.app.databinding.FragmentFtueAccountCreatedBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents @@ -47,7 +50,9 @@ class FtueAuthAccountCreatedFragment @Inject constructor( } private fun setupViews() { - views.accountCreatedSubtitle.text = getString(R.string.ftue_account_created_subtitle, activeSessionHolder.getActiveSession().myUserId) + val userId = activeSessionHolder.getActiveSession().myUserId + val subtitle = getString(R.string.ftue_account_created_subtitle, userId).toSpannable().styleMatchingText(userId, Typeface.BOLD) + views.accountCreatedSubtitle.text = subtitle views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PersonalizeProfile) } views.accountCreatedTakeMeHome.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) } views.accountCreatedTakeMeHomeCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt index d9086952da..58b1edddf8 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedLoginFragment.kt @@ -25,6 +25,7 @@ import androidx.autofill.HintConstants import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import im.vector.app.R +import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.hideKeyboard @@ -41,8 +42,10 @@ import im.vector.app.features.login.render import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject class FtueAuthCombinedLoginFragment @Inject constructor( @@ -60,14 +63,18 @@ class FtueAuthCombinedLoginFragment @Inject constructor( views.loginRoot.realignPercentagesToParent() views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.loginPasswordInput.setOnImeDoneListener { submit() } - views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) } + views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.UserNameEnteredAction.Login(views.loginInput.content())) } views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) } } private fun setupSubmitButton() { views.loginSubmit.setOnClickListener { submit() } - observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit) - .launchIn(viewLifecycleOwner.lifecycleScope) + views.loginInput.clearErrorOnChange(viewLifecycleOwner) + views.loginPasswordInput.clearErrorOnChange(viewLifecycleOwner) + + combine(views.loginInput.editText().textChanges(), views.loginPasswordInput.editText().textChanges()) { account, password -> + views.loginSubmit.isEnabled = account.isNotEmpty() && password.isNotEmpty() + }.launchIn(viewLifecycleOwner.lifecycleScope) } private fun submit() { @@ -105,7 +112,6 @@ class FtueAuthCombinedLoginFragment @Inject constructor( setupAutoFill() views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl() - views.selectedServerDescription.text = state.selectedHomeserver.description if (state.isLoading) { // Ensure password is hidden diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt index 7df1940970..639045b5c0 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthCombinedRegisterFragment.kt @@ -28,11 +28,14 @@ import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R +import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.hasSurroundingSpaces import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hidePassword +import im.vector.app.core.extensions.isMatrixId +import im.vector.app.core.extensions.onTextChange import im.vector.app.core.extensions.realignPercentagesToParent import im.vector.app.core.extensions.setOnFocusLostListener import im.vector.app.core.extensions.setOnImeDoneListener @@ -46,6 +49,7 @@ import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction import im.vector.app.features.onboarding.OnboardingViewEvents import im.vector.app.features.onboarding.OnboardingViewState +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider import org.matrix.android.sdk.api.failure.isHomeserverUnavailable @@ -55,8 +59,11 @@ import org.matrix.android.sdk.api.failure.isLoginEmailUnknown import org.matrix.android.sdk.api.failure.isRegistrationDisabled import org.matrix.android.sdk.api.failure.isUsernameInUse import org.matrix.android.sdk.api.failure.isWeakPassword +import reactivecircus.flowbinding.android.widget.textChanges import javax.inject.Inject +private const val MINIMUM_PASSWORD_LENGTH = 8 + class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment() { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedRegisterBinding { @@ -69,15 +76,27 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu views.createAccountRoot.realignPercentagesToParent() views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) } views.createAccountPasswordInput.setOnImeDoneListener { submit() } + + views.createAccountInput.onTextChange(viewLifecycleOwner) { + viewModel.handle(OnboardingAction.ResetSelectedRegistrationUserName) + views.createAccountEntryFooter.text = "" + } + views.createAccountInput.setOnFocusLostListener { - viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.createAccountInput.content())) + viewModel.handle(OnboardingAction.UserNameEnteredAction.Registration(views.createAccountInput.content())) } } private fun setupSubmitButton() { views.createAccountSubmit.setOnClickListener { submit() } - observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit) - .launchIn(viewLifecycleOwner.lifecycleScope) + views.createAccountInput.clearErrorOnChange(viewLifecycleOwner) + views.createAccountPasswordInput.clearErrorOnChange(viewLifecycleOwner) + + combine(views.createAccountInput.editText().textChanges(), views.createAccountPasswordInput.editText().textChanges()) { account, password -> + val accountIsValid = account.isNotEmpty() + val passwordIsValid = password.length >= MINIMUM_PASSWORD_LENGTH + views.createAccountSubmit.isEnabled = accountIsValid && passwordIsValid + }.launchIn(viewLifecycleOwner.lifecycleScope) } private fun submit() { @@ -103,7 +122,12 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu } if (error == 0) { - viewModel.handle(AuthenticateAction.Register(login, password, getString(R.string.login_default_session_public_name))) + val initialDeviceName = getString(R.string.login_default_session_public_name) + val registerAction = when { + login.isMatrixId() -> AuthenticateAction.RegisterWithMatrixId(login, password, initialDeviceName) + else -> AuthenticateAction.Register(login, password, initialDeviceName) + } + viewModel.handle(registerAction) } } } @@ -153,17 +177,25 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu override fun updateWithState(state: OnboardingViewState) { setupUi(state) setupAutoFill() + } + private fun setupUi(state: OnboardingViewState) { views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl() - views.selectedServerDescription.text = state.selectedHomeserver.description if (state.isLoading) { // Ensure password is hidden views.createAccountPasswordInput.editText().hidePassword() } - } - private fun setupUi(state: OnboardingViewState) { + views.createAccountEntryFooter.text = when { + state.registrationState.isUserNameAvailable -> getString( + R.string.ftue_auth_create_account_username_entry_footer, + state.registrationState.selectedMatrixId + ) + + else -> "" + } + when (state.selectedHomeserver.preferredLoginMode) { is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders) else -> hideSsoProviders() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt index 1d85c75fa1..61da7e0d18 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthEmailEntryFragment.kt @@ -20,14 +20,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import im.vector.app.R import im.vector.app.core.extensions.associateContentStateWith import im.vector.app.core.extensions.autofillEmail import im.vector.app.core.extensions.clearErrorOnChange import im.vector.app.core.extensions.content import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtueEmailInputBinding import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.RegisterAction import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import javax.inject.Inject @@ -56,6 +59,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(email)))) } + override fun updateWithState(state: OnboardingViewState) { + views.emailEntryHeaderSubtitle.text = getString(R.string.ftue_auth_email_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl()) + } + override fun onError(throwable: Throwable) { views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt index 905af75639..32291ecb6e 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPhoneEntryFragment.kt @@ -27,8 +27,10 @@ import im.vector.app.core.extensions.autofillPhoneNumber import im.vector.app.core.extensions.content import im.vector.app.core.extensions.editText import im.vector.app.core.extensions.setOnImeDoneListener +import im.vector.app.core.extensions.toReducedUrl import im.vector.app.databinding.FragmentFtuePhoneInputBinding import im.vector.app.features.onboarding.OnboardingAction +import im.vector.app.features.onboarding.OnboardingViewState import im.vector.app.features.onboarding.RegisterAction import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -77,6 +79,10 @@ class FtueAuthPhoneEntryFragment @Inject constructor( } } + override fun updateWithState(state: OnboardingViewState) { + views.phoneEntryHeaderSubtitle.text = getString(R.string.ftue_auth_phone_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl()) + } + override fun onError(throwable: Throwable) { views.phoneEntryInput.error = errorFormatter.toHumanReadable(throwable) } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt index 41e24e96c2..721423ecdf 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthResetPasswordBreakerFragment.kt @@ -57,7 +57,7 @@ class FtueAuthResetPasswordBreakerFragment : AbstractFtueAuthFragment() { @@ -53,6 +56,13 @@ class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment { - return combine( - username.hasContentFlow { it.trim() }, - password.hasContentFlow(), - transform = { usernameHasContent, passwordHasContent -> usernameHasContent && passwordHasContent } - ).onEach { - username.error = null - password.error = null - submit.isEnabled = it - } -} - fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) { - true -> R.drawable.bg_gradient_ftue_breaker + true -> R.drawable.bg_gradient_ftue_breaker false -> R.drawable.bg_color_background } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt index f168536575..371c618d54 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/terms/FtueAuthTermsFragment.kt @@ -114,7 +114,9 @@ class FtueAuthTermsFragment @Inject constructor( } override fun updateWithState(state: OnboardingViewState) { - policyController.homeServer = state.selectedHomeserver.userFacingUrl.toReducedUrl() + val homeserverName = state.selectedHomeserver.userFacingUrl.toReducedUrl() + views.termsHeaderSubtitle.text = getString(R.string.ftue_auth_terms_subtitle, homeserverName) + policyController.homeServer = homeserverName renderState() } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt index 1b7d35879e..7a293d022e 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricAuthError.kt @@ -33,6 +33,6 @@ class BiometricAuthError(val code: Int, message: String) : Throwable(message) { val isAuthPermanentlyDisabledError: Boolean get() = code == BiometricPrompt.ERROR_LOCKOUT_PERMANENT companion object { - private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT) + private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt index 1b510f5983..a34b284193 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/biometrics/BiometricHelper.kt @@ -73,26 +73,30 @@ class BiometricHelper @Inject constructor( /** * Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used. */ - val canUseWeakBiometricAuth: Boolean get() = - configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS + val canUseWeakBiometricAuth: Boolean + get() = + configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS /** * Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used. */ - val canUseStrongBiometricAuth: Boolean get() = - configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS + val canUseStrongBiometricAuth: Boolean + get() = + configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS /** * Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.). */ - val canUseDeviceCredentialsAuth: Boolean get() = - configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS + val canUseDeviceCredentialsAuth: Boolean + get() = + configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS /** * Returns true if any system authentication method (biometric weak/strong or device credentials) can be used. */ @VisibleForTesting(otherwise = PRIVATE) - internal val canUseAnySystemAuth: Boolean get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth + internal val canUseAnySystemAuth: Boolean + get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth /** * Returns true if any system authentication method and there is a valid associated key. @@ -153,9 +157,9 @@ class BiometricHelper @Inject constructor( @SuppressLint("NewApi") @OptIn(ExperimentalCoroutinesApi::class) private fun authenticateInternal( - activity: FragmentActivity, - checkSystemKeyExists: Boolean, - cryptoObject: BiometricPrompt.CryptoObject? = null, + activity: FragmentActivity, + checkSystemKeyExists: Boolean, + cryptoObject: BiometricPrompt.CryptoObject? = null, ): Flow { if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false) @@ -189,9 +193,9 @@ class BiometricHelper @Inject constructor( @VisibleForTesting(otherwise = PRIVATE) internal fun authenticateWithPromptInternal( - activity: FragmentActivity, - cryptoObject: BiometricPrompt.CryptoObject? = null, - channel: Channel, + activity: FragmentActivity, + cryptoObject: BiometricPrompt.CryptoObject? = null, + channel: Channel, ): BiometricPrompt { val executor = ContextCompat.getMainExecutor(context) val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher()) diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt index a8690f69d2..4a7bce8a52 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/crypto/LockScreenKeyRepository.kt @@ -36,7 +36,7 @@ class LockScreenKeyRepository( private val systemKeyAlias = "$baseName.system" private val pinCodeCrypto: KeyStoreCrypto by lazy { - keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false) + keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false) } private val systemKeyCrypto: KeyStoreCrypto by lazy { keyStoreCryptoFactory.provide(systemKeyAlias, keyNeedsUserAuthentication = true) diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt index 764da8cceb..0e6fdfb61e 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenFragment.kt @@ -114,15 +114,15 @@ class LockScreenFragment : VectorBaseFragment() { private fun handleEvent(viewEvent: LockScreenViewEvent) { when (viewEvent) { is LockScreenViewEvent.CodeCreationComplete -> lockScreenListener?.onPinCodeCreated() - is LockScreenViewEvent.ClearPinCode -> { + is LockScreenViewEvent.ClearPinCode -> { if (viewEvent.confirmationFailed) { lockScreenListener?.onNewCodeValidationFailed() } views.codeView.clearCode() } - is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method) - is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method) - is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable) + is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method) + is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method) + is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt index a240af243a..39d0937323 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/ui/LockScreenViewModel.kt @@ -94,7 +94,7 @@ class LockScreenViewModel @AssistedInject constructor( override fun handle(action: LockScreenAction) { when (action) { - is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value) + is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value) is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity) } } diff --git a/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt b/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt index fa1d7d5559..1760cd6c80 100644 --- a/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt +++ b/vector/src/main/java/im/vector/app/features/pin/lockscreen/utils/DevicePromptCheck.kt @@ -48,15 +48,15 @@ object DevicePromptCheck { * See [this OP forum thread](https://forums.oneplus.com/threads/oneplus-7-pro-fingerprint-biometricprompt-does-not-show.1035821/). */ private val isOnePlusDeviceWithNoBiometricUI: Boolean = - Build.BRAND.equals("OnePlus", ignoreCase = true) && - !onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) && - Build.VERSION.SDK_INT < Build.VERSION_CODES.R + Build.BRAND.equals("OnePlus", ignoreCase = true) && + !onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) && + Build.VERSION.SDK_INT < Build.VERSION_CODES.R /** * Some LG models don't seem to have a system biometric prompt at all. */ private val isLGDeviceWithNoBiometricUI: Boolean = - Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL) + Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL) /** * Check if this device is included in the list of devices with known Biometric Prompt issues. diff --git a/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt b/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt new file mode 100644 index 0000000000..ac77455d66 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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.redaction + +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +class CheckIfEventIsRedactedUseCase @Inject constructor( + private val session: Session, +) { + + suspend fun execute(roomId: String, eventId: String): Boolean { + Timber.d("checking if event is redacted for roomId=$roomId and eventId=$eventId") + return try { + session.eventService() + .getEvent(roomId, eventId) + .isRedacted() + .also { Timber.d("event isRedacted=$it") } + } catch (error: Exception) { + Timber.e(error, "error when getting event, it may not exist yet") + false + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt b/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt index d553c1a8ce..78c06d5969 100644 --- a/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/font/FontScaleSettingFragment.kt @@ -31,7 +31,7 @@ import javax.inject.Inject class FontScaleSettingFragment @Inject constructor( private val fontListController: FontScaleSettingController -) : VectorBaseFragment(), FontScaleSettingController.Callback { +) : VectorBaseFragment(), FontScaleSettingController.Callback { private val viewModel: FontScaleSettingViewModel by fragmentViewModel() diff --git a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt index 265cf3199e..b1a240e942 100644 --- a/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt +++ b/vector/src/main/java/im/vector/app/features/signout/soft/SoftLogoutController.kt @@ -94,10 +94,10 @@ class SoftLogoutController @Inject constructor( } private fun buildForm(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest) { - is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error) - is Success -> buildLoginSuccessItem(state) + is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error) + is Success -> buildLoginSuccessItem(state) is Loading, Uninitialized -> buildLoadingItem() - is Incomplete -> Unit + is Incomplete -> Unit } private fun buildLoadingItem() { @@ -116,11 +116,11 @@ class SoftLogoutController @Inject constructor( } private fun buildLoginSuccessItem(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest.invoke()) { - LoginMode.Password -> buildLoginPasswordForm(state) - is LoginMode.Sso -> buildLoginSSOForm() + LoginMode.Password -> buildLoginPasswordForm(state) + is LoginMode.Sso -> buildLoginSSOForm() is LoginMode.SsoAndPassword -> disambiguateLoginSSOAndPasswordForm(state) - LoginMode.Unsupported -> buildLoginUnsupportedForm() - LoginMode.Unknown, null -> Unit // Should not happen + LoginMode.Unsupported -> buildLoginUnsupportedForm() + LoginMode.Unknown, null -> Unit // Should not happen } private fun buildLoginPasswordForm(state: SoftLogoutViewState) { @@ -148,12 +148,12 @@ class SoftLogoutController @Inject constructor( private fun disambiguateLoginSSOAndPasswordForm(state: SoftLogoutViewState) { when (state.loginType) { - LoginType.PASSWORD -> buildLoginPasswordForm(state) - LoginType.SSO -> buildLoginSSOForm() + LoginType.PASSWORD -> buildLoginPasswordForm(state) + LoginType.SSO -> buildLoginSSOForm() LoginType.DIRECT, LoginType.CUSTOM, LoginType.UNSUPPORTED -> buildLoginUnsupportedForm() - LoginType.UNKNOWN -> Unit + LoginType.UNKNOWN -> Unit } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index 4286c12058..5b362690fa 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -110,6 +110,7 @@ class SpaceDirectoryController @Inject constructor( ?.filter { it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId) } + ?.filterNot { it.isUpgradedRoom(data) } ?: emptyList() if (flattenChildInfo.isEmpty()) { @@ -209,4 +210,7 @@ class SpaceDirectoryController @Inject constructor( } } } + + private fun SpaceChildInfo.isUpgradedRoom(data: SpaceDirectoryState) = + data.knownRoomSummaries.any { it.roomId == childRoomId && it.versioningState.isUpgraded() } } diff --git a/vector/src/main/res/drawable/bg_live_location_marker_popup.xml b/vector/src/main/res/drawable/bg_live_location_marker_popup.xml new file mode 100644 index 0000000000..ede7daef0c --- /dev/null +++ b/vector/src/main/res/drawable/bg_live_location_marker_popup.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/vector/src/main/res/layout/fragment_ftue_combined_login.xml b/vector/src/main/res/layout/fragment_ftue_combined_login.xml index d50fdb6394..b23ea9d7cc 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_login.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_login.xml @@ -140,7 +140,7 @@ style="@style/Widget.Vector.TextInputLayout.Username" android:layout_width="0dp" android:layout_height="wrap_content" - android:hint="@string/username" + android:hint="@string/ftue_auth_login_username_entry" app:layout_constraintBottom_toTopOf="@id/entrySpacing" app:layout_constraintEnd_toEndOf="@id/loginGutterEnd" app:layout_constraintStart_toStartOf="@id/loginGutterStart" diff --git a/vector/src/main/res/layout/fragment_ftue_combined_register.xml b/vector/src/main/res/layout/fragment_ftue_combined_register.xml index 304e5b475f..0a7b5b57d0 100644 --- a/vector/src/main/res/layout/fragment_ftue_combined_register.xml +++ b/vector/src/main/res/layout/fragment_ftue_combined_register.xml @@ -34,8 +34,8 @@ android:layout_height="52dp" app:layout_constraintBottom_toTopOf="@id/createAccountHeaderIcon" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0" - app:layout_constraintVertical_chainStyle="packed" /> + app:layout_constraintVertical_chainStyle="packed" + app:layout_constraintVertical_bias="0" /> - - + app:layout_constraintTop_toBottomOf="@id/createAccountHeaderIcon" /> + app:layout_constraintTop_toBottomOf="@id/createAccountHeaderTitle" /> - - + app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader" + tools:text="matrix.org" />