diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..e95387dd6c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,69 @@ +name: Bug report for the Element Android app +description: Report any issues that you have found with the Element app. Please [check open issues](https://github.com/vector-im/element-android/issues) first, in case it has already been reported. +labels: [T-Defect] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + Please report security issues by email to security@matrix.org + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce + description: Please attach screenshots, videos or logs if you can. + placeholder: Tell us what you see! + value: | + 1. Where are you starting? What can you see? + 2. What do you click? + 3. More steps… + validations: + required: true + - type: textarea + id: expected-result + attributes: + label: What did you expect? + placeholder: Tell us what you expected to happen in as much detail as you can. + validations: + required: true + - type: textarea + id: actual-result + attributes: + label: What happened? + placeholder: Tell us what went wrong + validations: + required: true + - type: input + id: device + attributes: + label: Your phone model + placeholder: e.g. Samsung S6 + validations: + required: false + - type: input + id: os + attributes: + label: Operating system version + placeholder: e.g. Android 10.0 + validations: + required: false + - type: input + id: version + attributes: + label: Application version and app store + description: You can find the version information in Settings -> Help & About. + placeholder: e.g. Element version 1.7.34, olm version 3.2.3 from F-Droid + validations: + required: false + - type: dropdown + id: rageshake + attributes: + label: Have you submitted a rageshake? + description: | + Did you know that you can shake your phone to submit logs for this issue? Trigger the defect, then shake your phone and you will see a popup asking if you would like to open the bug report screen. Click YES, and describe the issue, mentioning that you have also filed a bug. Submit the report to send anonymous logs to the developers. + options: + - 'Yes' + - 'No' + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index d7c3506fa0..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve Element -title: '' -labels: '' -assignees: '' - ---- - -#### Describe the bug -A clear and concise description of what the bug is. - -#### To Reproduce -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -#### Expected behavior -A clear and concise description of what you expected to happen. - -#### Screenshots -If applicable, add screenshots to help explain your problem. - -#### Smartphone (please complete the following information): - - Device: [e.g. Samsung S6] - - OS: [e.g. Android 6.0] - -#### Additional context - - App version and store [e.g. 1.0.0 - F-Droid] - - Homeserver: [e.g. matrix.org] - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml new file mode 100644 index 0000000000..5d9cfb3c88 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -0,0 +1,36 @@ +name: Enhancement request +description: Do you have a suggestion or feature request? +labels: [T-Enhancement] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to propose a new feature or make a suggestion. + - type: textarea + id: usecase + attributes: + label: Your use case + description: What would you like to be able to do? Please feel welcome to include screenshots or mock ups. + placeholder: Tell us what you would like to do! + value: | + #### What would you like to do? + + #### Why would you like to do it? + + #### How would you like to achieve it? + validations: + required: true + - type: textarea + id: alternative + attributes: + label: Have you considered any alternatives? + placeholder: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + placeholder: Is there anything else you'd like to add? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index da96d461c5..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: type:suggestion -assignees: '' - ---- - -#### Is your feature request related to a problem? Please describe. -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -#### Describe the solution you'd like. -A clear and concise description of what you want to happen. - -#### Describe alternatives you've considered. -A clear and concise description of any alternative solutions or features you've considered. - -#### Additional context -Add any other context or screenshots about the feature request here. diff --git a/changelog.d/1823.bugfix b/changelog.d/1823.bugfix new file mode 100644 index 0000000000..8252e1826f --- /dev/null +++ b/changelog.d/1823.bugfix @@ -0,0 +1 @@ +- Add mxid to autocomplete suggestion if more than one user in a room has the same displayname diff --git a/changelog.d/3853.feature b/changelog.d/3853.feature new file mode 100644 index 0000000000..86c19d2c22 --- /dev/null +++ b/changelog.d/3853.feature @@ -0,0 +1 @@ +Call: show dialog for some ended reasons. \ No newline at end of file diff --git a/changelog.d/3887.bugfix b/changelog.d/3887.bugfix new file mode 100644 index 0000000000..eecd2cea19 --- /dev/null +++ b/changelog.d/3887.bugfix @@ -0,0 +1 @@ +Message edition is not rendered in e2e rooms after pagination \ No newline at end of file diff --git a/changelog.d/pr-3883.misc b/changelog.d/pr-3883.misc new file mode 100644 index 0000000000..468e5e4927 --- /dev/null +++ b/changelog.d/pr-3883.misc @@ -0,0 +1 @@ +Issue templates: modernise and sync with element-web diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index e30b63e751..90be941b6f 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -169,7 +169,7 @@ dependencies { implementation 'com.otaliastudios:transcoder:0.10.3' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.30' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.5.1' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index 88aa432fb3..758c7aa5b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -17,6 +17,9 @@ package org.matrix.android.sdk.internal.database import com.zhuinden.monarchy.Monarchy +import io.realm.RealmConfiguration +import io.realm.RealmResults +import kotlinx.coroutines.launch import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -24,20 +27,15 @@ import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import org.matrix.android.sdk.internal.crypto.EventDecryptor import timber.log.Timber import javax.inject.Inject internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, - private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, - private val eventDecryptor: EventDecryptor) + private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>) : RealmLiveEntityObserver(realmConfiguration) { - override val query = Monarchy.Query { - it.where(EventInsertEntity::class.java) + override val query = Monarchy.Query { + it.where(EventInsertEntity::class.java).equalTo(EventInsertEntityFields.CAN_BE_PROCESSED, true) } override fun onChange(results: RealmResults) { @@ -86,23 +84,6 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real } } -// private fun decryptIfNeeded(event: Event) { -// if (event.isEncrypted() && event.mxDecryptionResult == null) { -// try { -// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "") -// event.mxDecryptionResult = OlmDecryptionResult( -// payload = result.clearEvent, -// senderKey = result.senderCurve25519Key, -// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, -// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain -// ) -// } catch (e: MXCryptoError) { -// Timber.v("Failed to decrypt event") -// // TODO -> we should keep track of this and retry, or some processing will never be handled -// } -// } -// } - private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { return processors.any { it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) 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 28ae4d8bfd..aa96ca5e1a 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 @@ -29,6 +29,7 @@ import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFie import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields @@ -46,7 +47,7 @@ import timber.log.Timber internal object RealmSessionStoreMigration : RealmMigration { - const val SESSION_STORE_SCHEMA_VERSION = 16L + const val SESSION_STORE_SCHEMA_VERSION = 17L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Session from $oldVersion to $newVersion") @@ -67,6 +68,7 @@ internal object RealmSessionStoreMigration : RealmMigration { if (oldVersion <= 13) migrateTo14(realm) if (oldVersion <= 14) migrateTo15(realm) if (oldVersion <= 15) migrateTo16(realm) + if (oldVersion <= 16) migrateTo17(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -330,4 +332,10 @@ internal object RealmSessionStoreMigration : RealmMigration { obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0) } } + + private fun migrateTo17(realm: DynamicRealm) { + Timber.d("Step 16 -> 17") + realm.schema.get("EventInsertEntity") + ?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index c9edbcd889..4dc8712afb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider import io.realm.RealmObject import io.realm.annotations.Index +import org.matrix.android.sdk.internal.extensions.assertIsManaged internal open class EventEntity(@Index var eventId: String = "", @Index var roomId: String = "", @@ -56,15 +57,22 @@ internal open class EventEntity(@Index var eventId: String = "", companion object fun setDecryptionResult(result: MXEventDecryptionResult) { + assertIsManaged() val decryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain ) - val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) + val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java) decryptionResultJson = adapter.toJson(decryptionResult) decryptionErrorCode = null decryptionErrorReason = null + + // If we have an EventInsertEntity for the eventId we make sures it can be processed now. + realm.where(EventInsertEntity::class.java) + .equalTo(EventInsertEntityFields.EVENT_ID, eventId) + .findFirst() + ?.canBeProcessed = true } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt index f4426207be..5cfd306d2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt @@ -23,7 +23,12 @@ import io.realm.RealmObject * in EventEntity table. */ internal open class EventInsertEntity(var eventId: String = "", - var eventType: String = "" + var eventType: String = "", + /** + * This flag will be used to filter EventInsertEntity in EventInsertLiveObserver. + * Currently it's set to false when the event content is encrypted. + */ + var canBeProcessed: Boolean = true ) : RealmObject() { private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index 0bf62a19fe..57e24cf88f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -24,6 +24,7 @@ import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.events.model.EventType internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { val eventEntity = realm.where() @@ -31,7 +32,8 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse .equalTo(EventEntityFields.ROOM_ID, roomId) .findFirst() return if (eventEntity == null) { - val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply { + val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null + val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply { this.insertType = insertType } realm.insert(insertEntity) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt index 2116063626..71ba71b915 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -86,13 +86,16 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) if (matrixError != null) { - if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { - // Also send this error to the globalErrorReceiver, for a global management - globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri)) - } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ - && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { - // Also send this error to the globalErrorReceiver, for a global management - globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse())) + when { + matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank() -> { + // Also send this error to the globalErrorReceiver, for a global management + globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri)) + } + httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ + && matrixError.code == MatrixError.M_UNKNOWN_TOKEN -> { + // Also send this error to the globalErrorReceiver, for a global management + globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse())) + } } return Failure.ServerError(matrixError, httpCode) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index e98e5646af..13095fbd58 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -77,7 +77,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val timelineEvent = timelineEventMapper.map(timelineEventEntity) timelineInput.onLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent) taskExecutor.executorScope.asyncTransaction(monarchy) { realm -> - val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { + val eventInsertEntity = EventInsertEntity(event.eventId, event.type, canBeProcessed = true).apply { this.insertType = EventInsertType.LOCAL_ECHO } realm.insert(eventInsertEntity) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index 3517f26c5d..721dae0b1b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -106,7 +106,8 @@ internal class TimelineEventDecryptor @Inject constructor( val result = cryptoService.decryptEvent(request.event, timelineId) Timber.v("Successfully decrypted event ${event.eventId}") realm.executeTransaction { - EventEntity.where(it, eventId = event.eventId ?: "") + val eventId = event.eventId ?: "" + EventEntity.where(it, eventId = eventId) .findFirst() ?.setDecryptionResult(result) } diff --git a/vector/build.gradle b/vector/build.gradle index b47b3ddb61..c1bf57ac57 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -143,8 +143,10 @@ android { resValue "bool", "useLoginV2", "false" // NotificationSettingsV2 is disabled. To be released in conjunction with iOS/Web - resValue "bool", "useNotificationSettingsV1", "true" - resValue "bool", "useNotificationSettingsV2", "false" + def useNotificationSettingsV2 = false + buildConfigField "Boolean", "USE_NOTIFICATION_SETTINGS_V2", "${useNotificationSettingsV2}" + resValue "bool", "useNotificationSettingsV1", "${!useNotificationSettingsV2}" + resValue "bool", "useNotificationSettingsV2", "${useNotificationSettingsV2}" buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" @@ -366,7 +368,7 @@ dependencies { implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.30' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31' // rx implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index cce0c2a66f..6b42f1e428 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -106,6 +106,7 @@ import im.vector.app.features.roomprofile.RoomProfileFragment import im.vector.app.features.roomprofile.alias.RoomAliasFragment import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleChooseRestrictedFragment @@ -717,6 +718,11 @@ interface FragmentModule { @FragmentKey(RoomBannedMemberListFragment::class) fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomNotificationSettingsFragment::class) + fun bindRoomNotificationSettingsFragment(fragment: RoomNotificationSettingsFragment): Fragment + @Binds @IntoMap @FragmentKey(SearchFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt new file mode 100644 index 0000000000..4608f2b1ce --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/NotificationSettingsFooterItem.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.epoxy.profiles.notifications + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextWithColoredPart + +@EpoxyModelClass(layout = R.layout.item_notifications_footer) +abstract class NotificationSettingsFooterItem : VectorEpoxyModel() { + + @EpoxyAttribute + var encrypted: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + val accountSettingsString = holder.view.context.getString(R.string.room_settings_room_notifications_account_settings) + val manageNotificationsString = holder.view.context.getString( + R.string.room_settings_room_notifications_manage_notifications, + accountSettingsString + ) + val manageNotificationsBuilder = StringBuilder(manageNotificationsString) + if (encrypted) { + val encryptionNotice = holder.view.context.getString(R.string.room_settings_room_notifications_encryption_notice) + manageNotificationsBuilder.appendLine().append(encryptionNotice) + } + + holder.textView.setTextWithColoredPart( + manageNotificationsBuilder.toString(), + accountSettingsString, + underline = true + ) { + clickListener?.invoke(holder.textView) + } + } + + class Holder : VectorEpoxyHolder() { + val textView by bind(R.id.footerText) + } +} diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt new file mode 100644 index 0000000000..37d16ab6b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/RadioButtonItem.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.epoxy.profiles.notifications + +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick + +@EpoxyModelClass(layout = R.layout.item_radio) +abstract class RadioButtonItem : VectorEpoxyModel() { + + @EpoxyAttribute + var title: CharSequence? = null + + @StringRes + @EpoxyAttribute + var titleRes: Int? = null + + @EpoxyAttribute + var selected = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var listener: ClickListener + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.onClick(listener) + if (titleRes != null) { + holder.titleText.setText(titleRes!!) + } else { + holder.titleText.text = title + } + + if (selected) { + holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_on)) + holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_checked) + } else { + holder.radioImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_radio_off)) + holder.radioImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked) + } + } + + class Holder : VectorEpoxyHolder() { + val titleText by bind(R.id.actionTitle) + val radioImage by bind(R.id.radioIcon) + } +} diff --git a/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt new file mode 100644 index 0000000000..2dfe7be2e6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/profiles/notifications/TextHeaderItem.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.epoxy.profiles.notifications + +import android.widget.TextView +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_text_header) +abstract class TextHeaderItem : VectorEpoxyModel() { + + @EpoxyAttribute + var text: String? = null + + @StringRes + @EpoxyAttribute + var textRes: Int? = null + + override fun bind(holder: Holder) { + super.bind(holder) + val textResource = textRes + if (textResource != null) { + holder.textView.setText(textResource) + } else { + holder.textView.text = text + } + } + + class Holder : VectorEpoxyHolder() { + val textView by bind(R.id.headerText) + } +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index bb991ac32c..1c424f7071 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -65,6 +65,23 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, val coloredPart = resources.getString(coloredTextRes) // Insert colored part into the full text val fullText = resources.getString(fullTextRes, coloredPart) + + setTextWithColoredPart(fullText, coloredPart, colorAttribute, underline, onClick) +} + +/** + * Set text with a colored part + * @param fullText The full text. + * @param coloredPart The colored part of the text + * @param colorAttribute attribute of the color. Default to colorPrimary + * @param underline true to also underline the text. Default to false + * @param onClick attributes to handle click on the colored part if needed + */ +fun TextView.setTextWithColoredPart(fullText: String, + coloredPart: String, + @AttrRes colorAttribute: Int = R.attr.colorPrimary, + underline: Boolean = false, + onClick: (() -> Unit)? = null) { val color = ThemeUtils.getColor(context, colorAttribute) val foregroundSpan = ForegroundColorSpan(color) diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt index ecc607f08d..aa0c10e0a2 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -71,6 +71,23 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, val members = room.getRoomMembers(queryParams) .asSequence() .sortedBy { it.displayName } + .disambiguate() controller.setData(members.toList()) } } + +private fun Sequence.disambiguate(): Sequence { + val displayNames = hashMapOf().also { map -> + for (item in this) { + item.displayName?.lowercase()?.also { displayName -> + map[displayName] = map.getOrPut(displayName, { 0 }) + 1 + } + } + } + + return map { roomMemberSummary -> + if (displayNames[roomMemberSummary.displayName?.lowercase()] ?: 0 > 1) { + roomMemberSummary.copy(displayName = roomMemberSummary.displayName + " " + roomMemberSummary.userId) + } else roomMemberSummary + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index d3719a22a6..f71dcc0635 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -30,6 +30,7 @@ import android.util.Rational import android.view.MenuItem import android.view.View import android.view.WindowManager +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.isInvisible @@ -66,6 +67,7 @@ import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.webrtc.EglBase import org.webrtc.RendererCommon import timber.log.Timber @@ -136,6 +138,12 @@ class VectorCallActivity : VectorBaseActivity(), CallContro renderState(it) } + callViewModel.asyncSubscribe(this, VectorCallViewState::callState) { + if (it is CallState.Ended) { + handleCallEnded(it) + } + } + callViewModel.viewEvents .observe() .observeOn(AndroidSchedulers.mainThread()) @@ -321,9 +329,16 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } } is CallState.Ended -> { - finish() + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false + views.callInfoGroup.isVisible = true + views.callToolbar.setSubtitle(R.string.call_ended) + configureCallInfo(state) } - null -> { + else -> { + views.fullscreenRenderer.isVisible = false + views.pipRendererWrapper.isVisible = false + views.callInfoGroup.isInvisible = true } } } @@ -343,7 +358,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro is CallState.Answering -> { views.fullscreenRenderer.isVisible = false views.callInfoGroup.isVisible = false - // showLoading() } is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { @@ -358,17 +372,47 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callInfoGroup.isVisible = false } } else { - // showLoading() + views.callInfoGroup.isVisible = false } } - is CallState.Ended -> { - finish() - } - null -> { + else -> { + views.fullscreenRenderer.isVisible = false + views.callInfoGroup.isVisible = false } } } + private fun handleCallEnded(callState: CallState.Ended) { + if (isInPictureInPictureModeSafe()) { + val startIntent = Intent(this, VectorCallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + } + startActivity(startIntent) + } + when (callState.reason) { + EndCallReason.USER_BUSY -> { + showEndCallDialog(R.string.call_ended_user_busy_title, R.string.call_ended_user_busy_description) + } + EndCallReason.INVITE_TIMEOUT -> { + showEndCallDialog(R.string.call_ended_invite_timeout_title, R.string.call_error_user_not_responding) + } + else -> { + finish() + } + } + } + + private fun showEndCallDialog(@StringRes title: Int, @StringRes description: Int) { + MaterialAlertDialogBuilder(this) + .setTitle(title) + .setMessage(description) + .setNegativeButton(R.string.ok, null) + .setOnDismissListener { + finish() + } + .show() + } + private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { state.callInfo?.opponentUserItem?.let { val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur) @@ -473,9 +517,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun handleViewEvents(event: VectorCallViewEvents?) { Timber.tag(loggerTag.value).v("handleViewEvents $event") when (event) { - VectorCallViewEvents.DismissNoCall -> { - finish() - } is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } @@ -498,7 +539,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro // TODO ask to use default stun, etc... MaterialAlertDialogBuilder(this) .setTitle(R.string.call_failed_no_connection) - .setMessage(getString(R.string.call_failed_no_connection_description)) + .setMessage(R.string.call_failed_no_connection_description) .setNegativeButton(R.string.ok) { _, _ -> callViewModel.handle(VectorCallViewActions.EndCall) } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt index 91c3154d0a..9f19429c00 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.session.call.TurnServerResponse sealed class VectorCallViewEvents : VectorViewEvents { - object DismissNoCall : VectorCallViewEvents() data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents() data class ShowSoundDeviceChooser( val available: Set, diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index cfe66c187e..63ba83bdbc 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -137,9 +137,7 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { - if (call == null) { - _viewEvents.post(VectorCallViewEvents.DismissNoCall) - } else { + if (call != null) { updateOtherKnownCall(call) } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index db4b313f86..2d39fda2e3 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -39,7 +39,9 @@ import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -91,6 +93,7 @@ private const val STREAM_ID = "userMedia" private const val AUDIO_TRACK_ID = "${STREAM_ID}a0" private const val VIDEO_TRACK_ID = "${STREAM_ID}v0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() +private const val INVITE_TIMEOUT_IN_MS = 60_000L private val loggerTag = LoggerTag("WebRtcCall", LoggerTag.VOIP) @@ -165,6 +168,8 @@ class WebRtcCall( } } + private var inviteTimeout: Deferred? = null + // Mute status var micMuted = false private set @@ -239,6 +244,10 @@ class WebRtcCall( if (mxCall.state == CallState.CreateOffer) { // send offer to peer mxCall.offerSdp(sessionDescription.description) + inviteTimeout = async { + delay(INVITE_TIMEOUT_IN_MS) + endCall(EndCallReason.INVITE_TIMEOUT) + } } else { mxCall.negotiate(sessionDescription.description, SdpType.OFFER) } @@ -807,7 +816,7 @@ class WebRtcCall( return@launch } val reject = mxCall.state is CallState.LocalRinging - terminate(EndCallReason.USER_HANGUP, reject) + terminate(reason, reject) if (reject) { mxCall.reject() } else { @@ -824,6 +833,8 @@ class WebRtcCall( val cameraManager = context.getSystemService()!! cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } + inviteTimeout?.cancel() + inviteTimeout = null mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP) release() onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected) @@ -845,6 +856,8 @@ class WebRtcCall( } fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { + inviteTimeout?.cancel() + inviteTimeout = null sessionScope?.launch(dispatcher) { Timber.tag(loggerTag.value).v("onCallAnswerReceived ${callAnswerContent.callId}") val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) 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 4a563b563a..9ae6d4c39f 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 @@ -143,6 +143,8 @@ class HomeActivity : } } + override fun getCoordinatorLayout() = views.coordinatorLayout + override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater) override fun injectWith(injector: ScreenComponent) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionViewState.kt new file mode 100644 index 0000000000..d063e07ed7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionViewState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list.actions + +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewState + +data class RoomListQuickActionViewState( + val roomListActionsArgs: RoomListActionsArgs, + val notificationSettingsViewState: RoomNotificationSettingsViewState +) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt index 94f9aaf496..8c1bdc086f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -22,15 +22,23 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.R import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetGenericListBinding import im.vector.app.features.navigation.Navigator +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsAction +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewEvents +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsViewModel import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import javax.inject.Inject @Parcelize @@ -54,11 +62,13 @@ class RoomListQuickActionsBottomSheet : private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel @Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool - @Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory + @Inject lateinit var roomNotificationSettingsViewModelFactory: RoomNotificationSettingsViewModel.Factory @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController @Inject lateinit var navigator: Navigator + @Inject lateinit var errorFormatter: ErrorFormatter - private val viewModel: RoomListQuickActionsViewModel by fragmentViewModel(RoomListQuickActionsViewModel::class) + private val roomListActionsArgs: RoomListActionsArgs by args() + private val viewModel: RoomNotificationSettingsViewModel by fragmentViewModel(RoomNotificationSettingsViewModel::class) override val showExpanded = true @@ -80,6 +90,12 @@ class RoomListQuickActionsBottomSheet : disableItemAnimation = true ) roomListActionsEpoxyController.listener = this + + viewModel.observeViewEvents { + when (it) { + is RoomNotificationSettingsViewEvents.Failure -> displayErrorDialog(it.throwable) + } + } } override fun onDestroyView() { @@ -89,7 +105,11 @@ class RoomListQuickActionsBottomSheet : } override fun invalidate() = withState(viewModel) { - roomListActionsEpoxyController.setData(it) + val roomListViewState = RoomListQuickActionViewState( + roomListActionsArgs, + it + ) + roomListActionsEpoxyController.setData(roomListViewState) super.invalidate() } @@ -103,6 +123,10 @@ class RoomListQuickActionsBottomSheet : } } + override fun didSelectRoomNotificationState(roomNotificationState: RoomNotificationState) { + viewModel.handle(RoomNotificationSettingsAction.SelectNotificationState(roomNotificationState)) + } + companion object { fun newInstance(roomId: String, mode: RoomListActionsArgs.Mode): RoomListQuickActionsBottomSheet { return RoomListQuickActionsBottomSheet().apply { @@ -110,4 +134,12 @@ class RoomListQuickActionsBottomSheet : } } } + + private fun displayErrorDialog(throwable: Throwable) { + MaterialAlertDialogBuilder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(throwable)) + .setPositiveButton(R.string.ok, null) + .show() + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index 4604159338..7e39156b18 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -15,13 +15,19 @@ */ package im.vector.app.features.home.room.list.actions +import androidx.annotation.StringRes import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.BuildConfig +import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem +import im.vector.app.core.epoxy.profiles.notifications.radioButtonItem import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.notifications.notificationOptions +import im.vector.app.features.roomprofile.notifications.notificationStateMapped import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -33,16 +39,19 @@ class RoomListQuickActionsEpoxyController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val colorProvider: ColorProvider, private val stringProvider: StringProvider -) : TypedEpoxyController() { +) : TypedEpoxyController() { var listener: Listener? = null - override fun buildModels(state: RoomListQuickActionsState) { - val roomSummary = state.roomSummary() ?: return + override fun buildModels(state: RoomListQuickActionViewState) { + val notificationViewState = state.notificationSettingsViewState + val roomSummary = notificationViewState.roomSummary() ?: return val host = this - val showAll = state.mode == RoomListActionsArgs.Mode.FULL + val isV2 = BuildConfig.USE_NOTIFICATION_SETTINGS_V2 + // V2 always shows full details as we no longer display the sheet from RoomProfile > Notifications + val showFull = state.roomListActionsArgs.mode == RoomListActionsArgs.Mode.FULL || isV2 - if (showAll) { + if (showFull) { // Preview, favorite, settings bottomSheetRoomPreviewItem { id("room_preview") @@ -63,17 +72,38 @@ class RoomListQuickActionsEpoxyController @Inject constructor( } } - val selectedRoomState = state.roomNotificationState() - RoomListQuickActionsSharedAction.NotificationsAllNoisy(roomSummary.roomId).toBottomSheetItem(0, selectedRoomState) - RoomListQuickActionsSharedAction.NotificationsAll(roomSummary.roomId).toBottomSheetItem(1, selectedRoomState) - RoomListQuickActionsSharedAction.NotificationsMentionsOnly(roomSummary.roomId).toBottomSheetItem(2, selectedRoomState) - RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) + if (isV2) { + notificationViewState.notificationOptions.forEach { notificationState -> + val title = titleForNotificationState(notificationState) + radioButtonItem { + id(notificationState.name) + titleRes(title) + selected(notificationViewState.notificationStateMapped() == notificationState) + listener { + host.listener?.didSelectRoomNotificationState(notificationState) + } + } + } + } else { + val selectedRoomState = notificationViewState.notificationState() + RoomListQuickActionsSharedAction.NotificationsAllNoisy(roomSummary.roomId).toBottomSheetItem(0, selectedRoomState) + RoomListQuickActionsSharedAction.NotificationsAll(roomSummary.roomId).toBottomSheetItem(1, selectedRoomState) + RoomListQuickActionsSharedAction.NotificationsMentionsOnly(roomSummary.roomId).toBottomSheetItem(2, selectedRoomState) + RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) + } - if (showAll) { - RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) + if (showFull) { + RoomListQuickActionsSharedAction.Leave(roomSummary.roomId, showIcon = !isV2).toBottomSheetItem(5) } } + @StringRes + private fun titleForNotificationState(notificationState: RoomNotificationState): Int? = when (notificationState) { + RoomNotificationState.ALL_MESSAGES_NOISY -> R.string.room_settings_all_messages + RoomNotificationState.MENTIONS_ONLY -> R.string.room_settings_mention_and_keyword_only + RoomNotificationState.MUTE -> R.string.room_settings_none + else -> null + } private fun RoomListQuickActionsSharedAction.toBottomSheetItem(index: Int, roomNotificationState: RoomNotificationState? = null) { val host = this@RoomListQuickActionsEpoxyController val selected = when (this) { @@ -86,7 +116,11 @@ class RoomListQuickActionsEpoxyController @Inject constructor( return bottomSheetActionItem { id("action_$index") selected(selected) - iconRes(iconResId) + if (iconResId != null) { + iconRes(iconResId) + } else { + showIcon(false) + } textRes(titleRes) destructive(this@toBottomSheetItem.destructive) listener { host.listener?.didSelectMenuAction(this@toBottomSheetItem) } @@ -95,5 +129,6 @@ class RoomListQuickActionsEpoxyController @Inject constructor( interface Listener { fun didSelectMenuAction(quickAction: RoomListQuickActionsSharedAction) + fun didSelectRoomNotificationState(roomNotificationState: RoomNotificationState) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt index 075dca0c52..6f93599d02 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt @@ -23,7 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction sealed class RoomListQuickActionsSharedAction( @StringRes val titleRes: Int, - @DrawableRes val iconResId: Int, + @DrawableRes val iconResId: Int?, val destructive: Boolean = false) : VectorSharedAction { @@ -60,9 +60,9 @@ sealed class RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_favorite_add, R.drawable.ic_star_24dp) - data class Leave(val roomId: String) : RoomListQuickActionsSharedAction( + data class Leave(val roomId: String, val showIcon: Boolean = true) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_leave, - R.drawable.ic_room_actions_leave, + if (showIcon) R.drawable.ic_room_actions_leave else null, true ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsState.kt deleted file mode 100644 index 2731620cec..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsState.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.home.room.list.actions - -import com.airbnb.mvrx.Async -import com.airbnb.mvrx.MvRxState -import com.airbnb.mvrx.Uninitialized -import org.matrix.android.sdk.api.session.room.model.RoomSummary -import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState - -data class RoomListQuickActionsState( - val roomId: String, - val mode: RoomListActionsArgs.Mode, - val roomSummary: Async = Uninitialized, - val roomNotificationState: Async = Uninitialized -) : MvRxState { - - constructor(args: RoomListActionsArgs) : this(roomId = args.roomId, mode = args.mode) -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt deleted file mode 100644 index 75e9459d2c..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsViewModel.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.app.features.home.room.list.actions - -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.ViewModelContext -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import dagger.assisted.AssistedFactory -import im.vector.app.core.platform.EmptyAction -import im.vector.app.core.platform.EmptyViewEvents -import im.vector.app.core.platform.VectorViewModel -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.rx.rx -import org.matrix.android.sdk.rx.unwrap - -class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initialState: RoomListQuickActionsState, - session: Session -) : VectorViewModel(initialState) { - - @AssistedFactory - interface Factory { - fun create(initialState: RoomListQuickActionsState): RoomListQuickActionsViewModel - } - - companion object : MvRxViewModelFactory { - - @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: RoomListQuickActionsState): RoomListQuickActionsViewModel? { - val fragment: RoomListQuickActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.roomListActionsViewModelFactory.create(state) - } - } - - private val room = session.getRoom(initialState.roomId)!! - - init { - observeRoomSummary() - observeNotificationState() - } - - private fun observeNotificationState() { - room - .rx() - .liveNotificationState() - .execute { - copy(roomNotificationState = it) - } - } - - private fun observeRoomSummary() { - room - .rx() - .liveRoomSummary() - .unwrap() - .execute { - copy(roomSummary = it) - } - } - - override fun handle(action: EmptyAction) { - // No op - } -} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 07ba442621..d28878283f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -39,6 +39,7 @@ import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.members.RoomMemberListFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.alias.RoomAliasFragment +import im.vector.app.features.roomprofile.notifications.RoomNotificationSettingsFragment import im.vector.app.features.roomprofile.permissions.RoomPermissionsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import javax.inject.Inject @@ -107,12 +108,13 @@ class RoomProfileActivity : .observe() .subscribe { sharedAction -> when (sharedAction) { - RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() - RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() - RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() - RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions() - RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() - RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() + RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() + RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() + RoomProfileSharedAction.OpenRoomAliasesSettings -> openRoomAlias() + RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions() + RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() + RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() + RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings() }.exhaustive } .disposeOnDestroy() @@ -162,6 +164,10 @@ class RoomProfileActivity : addFragmentToBackstack(R.id.simpleFragmentContainer, RoomBannedMemberListFragment::class.java, roomProfileArgs) } + private fun openRoomNotificationSettings() { + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomNotificationSettingsFragment::class.java, roomProfileArgs) + } + override fun configure(toolbar: MaterialToolbar) { configureToolbar(toolbar) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 14ddf896ca..4b37d038b5 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -30,6 +30,7 @@ import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder +import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.animations.AppBarStateChangeListener import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener @@ -253,9 +254,13 @@ class RoomProfileFragment @Inject constructor( } override fun onNotificationsClicked() { - RoomListQuickActionsBottomSheet - .newInstance(roomProfileArgs.roomId, RoomListActionsArgs.Mode.NOTIFICATIONS) - .show(childFragmentManager, "ROOM_PROFILE_NOTIFICATIONS") + if (BuildConfig.USE_NOTIFICATION_SETTINGS_V2) { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings) + } else { + RoomListQuickActionsBottomSheet + .newInstance(roomProfileArgs.roomId, RoomListActionsArgs.Mode.NOTIFICATIONS) + .show(childFragmentManager, "ROOM_PROFILE_NOTIFICATIONS") + } } override fun onUploadsClicked() { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt index 2a5775d1af..eb4ab56634 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileSharedAction.kt @@ -28,4 +28,5 @@ sealed class RoomProfileSharedAction : VectorSharedAction { object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction() + object OpenRoomNotificationSettings : RoomProfileSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsAction.kt new file mode 100644 index 0000000000..10c8861183 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.notifications + +import im.vector.app.core.platform.VectorViewModelAction +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState + +sealed class RoomNotificationSettingsAction : VectorViewModelAction { + data class SelectNotificationState(val notificationState: RoomNotificationState): RoomNotificationSettingsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsController.kt new file mode 100644 index 0000000000..9a2085a7e8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsController.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.notifications + +import androidx.annotation.StringRes +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.epoxy.profiles.notifications.notificationSettingsFooterItem +import im.vector.app.core.epoxy.profiles.notifications.radioButtonItem +import im.vector.app.core.epoxy.profiles.notifications.textHeaderItem +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import javax.inject.Inject + +class RoomNotificationSettingsController @Inject constructor() : TypedEpoxyController() { + + interface Callback { + fun didSelectRoomNotificationState(roomNotificationState: RoomNotificationState) + fun didSelectAccountSettingsLink() + } + + var callback: Callback? = null + + init { + setData(null) + } + + override fun buildModels(data: RoomNotificationSettingsViewState?) { + val host = this + data ?: return + + textHeaderItem { + id("roomNotificationSettingsHeader") + textRes(R.string.room_settings_room_notifications_notify_me) + } + data.notificationOptions.forEach { notificationState -> + val title = titleForNotificationState(notificationState) + radioButtonItem { + id(notificationState.name) + titleRes(title) + selected(data.notificationStateMapped() == notificationState) + listener { + host.callback?.didSelectRoomNotificationState(notificationState) + } + } + } + notificationSettingsFooterItem { + id("roomNotificationSettingsFooter") + encrypted(data.roomSummary()?.isEncrypted == true) + clickListener { + host.callback?.didSelectAccountSettingsLink() + } + } + } + + @StringRes + private fun titleForNotificationState(notificationState: RoomNotificationState): Int? = when (notificationState) { + RoomNotificationState.ALL_MESSAGES_NOISY -> R.string.room_settings_all_messages + RoomNotificationState.MENTIONS_ONLY -> R.string.room_settings_mention_and_keyword_only + RoomNotificationState.MUTE -> R.string.room_settings_none + else -> null + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt new file mode 100644 index 0000000000..ce0fde32c6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsFragment.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.notifications + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentRoomSettingGenericBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.settings.VectorSettingsActivity +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class RoomNotificationSettingsFragment @Inject constructor( + val viewModelFactory: RoomNotificationSettingsViewModel.Factory, + private val roomNotificationSettingsController: RoomNotificationSettingsController, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), + RoomNotificationSettingsController.Callback { + + private val viewModel: RoomNotificationSettingsViewModel by fragmentViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomSettingGenericBinding { + return FragmentRoomSettingGenericBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(views.roomSettingsToolbar) + roomNotificationSettingsController.callback = this + views.roomSettingsRecyclerView.configureWith(roomNotificationSettingsController, hasFixedSize = true) + setupWaitingView() + observeViewEvents() + } + + override fun onDestroyView() { + views.roomSettingsRecyclerView.cleanup() + roomNotificationSettingsController.callback = null + super.onDestroyView() + } + + private fun setupWaitingView() { + views.waitingView.waitingStatusText.setText(R.string.please_wait) + views.waitingView.waitingStatusText.isVisible = true + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is RoomNotificationSettingsViewEvents.Failure -> displayErrorDialog(it.throwable) + } + } + } + + override fun invalidate() = withState(viewModel) { viewState -> + roomNotificationSettingsController.setData(viewState) + views.waitingView.root.isVisible = viewState.isLoading + renderRoomSummary(viewState) + } + + override fun didSelectRoomNotificationState(roomNotificationState: RoomNotificationState) { + viewModel.handle(RoomNotificationSettingsAction.SelectNotificationState(roomNotificationState)) + } + + override fun didSelectAccountSettingsLink() { + navigator.openSettings(requireContext(), VectorSettingsActivity.EXTRA_DIRECT_ACCESS_NOTIFICATIONS) + } + + private fun renderRoomSummary(state: RoomNotificationSettingsViewState) { + state.roomSummary()?.let { + views.roomSettingsToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewEvents.kt new file mode 100644 index 0000000000..dda858283b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.notifications + +import im.vector.app.core.platform.VectorViewEvents + +sealed class RoomNotificationSettingsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : RoomNotificationSettingsViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewModel.kt new file mode 100644 index 0000000000..dd0535f51b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewModel.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.notifications + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.rx.rx +import org.matrix.android.sdk.rx.unwrap + +class RoomNotificationSettingsViewModel @AssistedInject constructor( + @Assisted initialState: RoomNotificationSettingsViewState, + session: Session +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: RoomNotificationSettingsViewState): RoomNotificationSettingsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomNotificationSettingsViewState): RoomNotificationSettingsViewModel { + val fragmentModelContext = (viewModelContext as FragmentViewModelContext) + return if (fragmentModelContext.fragment is RoomNotificationSettingsFragment) { + val fragment: RoomNotificationSettingsFragment = fragmentModelContext.fragment() + fragment.viewModelFactory.create(state) + } else { + val fragment: RoomListQuickActionsBottomSheet = fragmentModelContext.fragment() + fragment.roomNotificationSettingsViewModelFactory.create(state) + } + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + observeSummary() + observeNotificationState() + } + + private fun observeSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { async -> + copy(roomSummary = async) + } + } + + private fun observeNotificationState() { + room.rx() + .liveNotificationState() + .execute { + copy(notificationState = it) + } + } + + override fun handle(action: RoomNotificationSettingsAction) { + when (action) { + is RoomNotificationSettingsAction.SelectNotificationState -> handleSelectNotificationState(action) + } + } + + private fun handleSelectNotificationState(action: RoomNotificationSettingsAction.SelectNotificationState) { + setState { copy(isLoading = true) } + viewModelScope.launch { + runCatching { room.setRoomNotificationState(action.notificationState) } + .fold( + { + setState { + copy(isLoading = false, notificationState = Success(action.notificationState)) + } + }, + { + setState { + copy(isLoading = false) + } + _viewEvents.post(RoomNotificationSettingsViewEvents.Failure(it)) + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt new file mode 100644 index 0000000000..72e61fba70 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/notifications/RoomNotificationSettingsViewState.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomprofile.notifications + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.home.room.list.actions.RoomListActionsArgs +import im.vector.app.features.roomprofile.RoomProfileArgs +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState + +data class RoomNotificationSettingsViewState( + val roomId: String, + val roomSummary: Async = Uninitialized, + val isLoading: Boolean = false, + val notificationState: Async = Uninitialized +) : MvRxState { + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) + constructor(args: RoomListActionsArgs) : this(roomId = args.roomId) +} + +/** + * Used to map this old room notification settings to the new options in v2. + */ +val RoomNotificationSettingsViewState.notificationStateMapped: Async + get() { + if ((roomSummary()?.isEncrypted == true && notificationState() == RoomNotificationState.MENTIONS_ONLY) + || notificationState() == RoomNotificationState.ALL_MESSAGES) { + /** if in an encrypted room, mentions notifications are not supported so show "All Messages" as selected. + * Also in the new settings there is no notion of notifications without sound so it maps to noisy also + */ + return Success(RoomNotificationState.ALL_MESSAGES_NOISY) + } + return notificationState + } +/** + * Used to enumerate the new settings in notification settings v2. Notifications without sound and mentions in encrypted rooms not supported. + */ +val RoomNotificationSettingsViewState.notificationOptions: List + get() { + return if (roomSummary()?.isEncrypted == true) { + listOf(RoomNotificationState.ALL_MESSAGES_NOISY, RoomNotificationState.MUTE) + } else { + listOf(RoomNotificationState.ALL_MESSAGES_NOISY, RoomNotificationState.MENTIONS_ONLY, RoomNotificationState.MUTE) + } + } diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 8088969253..342ae11562 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -167,7 +167,6 @@ app:layout_constraintTop_toBottomOf="@id/participantNameText" tools:text="@string/call_resume_action" /> - - - - - - - diff --git a/vector/src/main/res/layout/item_notifications_footer.xml b/vector/src/main/res/layout/item_notifications_footer.xml new file mode 100644 index 0000000000..36db166e8f --- /dev/null +++ b/vector/src/main/res/layout/item_notifications_footer.xml @@ -0,0 +1,13 @@ + + diff --git a/vector/src/main/res/layout/item_radio.xml b/vector/src/main/res/layout/item_radio.xml new file mode 100644 index 0000000000..4cd5312dc0 --- /dev/null +++ b/vector/src/main/res/layout/item_radio.xml @@ -0,0 +1,40 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_text_header.xml b/vector/src/main/res/layout/item_text_header.xml new file mode 100644 index 0000000000..3f876e5ece --- /dev/null +++ b/vector/src/main/res/layout/item_text_header.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ded6256ccd..c41ff16623 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -752,6 +752,9 @@ %s held the call You held the call + User busy + The user you called is busy." + No answer The remote side failed to pick up. Media Connection Failed Cannot initialize the camera @@ -1051,6 +1054,8 @@ All messages Mentions only Mute + Mentions & Keywords only + None Favourite De-prioritize Direct Chat @@ -1441,6 +1446,10 @@ Access and visibility List this room in room directory Notifications + Notify me for + Please note that mentions & keyword notifications are not available in encrypted rooms on mobile. + You can manage notifications in %1$s. + Account settings Room Access Room History Readability Who can read history?