Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/keyword_notification_settings

This commit is contained in:
David Langley 2021-08-25 17:41:37 +01:00
commit daaa40b27a
41 changed files with 960 additions and 235 deletions

69
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View file

@ -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

View file

@ -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.

36
.github/ISSUE_TEMPLATE/enhancement.yml vendored Normal file
View file

@ -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

View file

@ -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.

1
changelog.d/3887.bugfix Normal file
View file

@ -0,0 +1 @@
Message edition is not rendered in e2e rooms after pagination

1
changelog.d/pr-3883.misc Normal file
View file

@ -0,0 +1 @@
Issue templates: modernise and sync with element-web

View file

@ -169,7 +169,7 @@ dependencies {
implementation 'com.otaliastudios:transcoder:0.10.3' implementation 'com.otaliastudios:transcoder:0.10.3'
// Phone number https://github.com/google/libphonenumber // 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 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1' testImplementation 'org.robolectric:robolectric:4.5.1'

View file

@ -17,6 +17,9 @@
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy 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.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity 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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>)
private val eventDecryptor: EventDecryptor)
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) { : RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventInsertEntity> { override val query = Monarchy.Query {
it.where(EventInsertEntity::class.java) it.where(EventInsertEntity::class.java).equalTo(EventInsertEntityFields.CAN_BE_PROCESSED, true)
} }
override fun onChange(results: RealmResults<EventInsertEntity>) { override fun onChange(results: RealmResults<EventInsertEntity>) {
@ -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 { private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any { return processors.any {
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)

View file

@ -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.EditAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields 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.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.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
@ -46,7 +47,7 @@ import timber.log.Timber
internal object RealmSessionStoreMigration : RealmMigration { 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) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Session from $oldVersion to $newVersion") Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
@ -67,6 +68,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
if (oldVersion <= 13) migrateTo14(realm) if (oldVersion <= 13) migrateTo14(realm)
if (oldVersion <= 14) migrateTo15(realm) if (oldVersion <= 14) migrateTo15(realm)
if (oldVersion <= 15) migrateTo16(realm) if (oldVersion <= 15) migrateTo16(realm)
if (oldVersion <= 16) migrateTo17(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -330,4 +332,10 @@ internal object RealmSessionStoreMigration : RealmMigration {
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0) 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)
}
} }

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.Index import io.realm.annotations.Index
import org.matrix.android.sdk.internal.extensions.assertIsManaged
internal open class EventEntity(@Index var eventId: String = "", internal open class EventEntity(@Index var eventId: String = "",
@Index var roomId: String = "", @Index var roomId: String = "",
@ -56,15 +57,22 @@ internal open class EventEntity(@Index var eventId: String = "",
companion object companion object
fun setDecryptionResult(result: MXEventDecryptionResult) { fun setDecryptionResult(result: MXEventDecryptionResult) {
assertIsManaged()
val decryptionResult = OlmDecryptionResult( val decryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
) )
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java) val adapter = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java)
decryptionResultJson = adapter.toJson(decryptionResult) decryptionResultJson = adapter.toJson(decryptionResult)
decryptionErrorCode = null decryptionErrorCode = null
decryptionErrorReason = 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
} }
} }

View file

@ -23,7 +23,12 @@ import io.realm.RealmObject
* in EventEntity table. * in EventEntity table.
*/ */
internal open class EventInsertEntity(var eventId: String = "", 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() { ) : RealmObject() {
private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name

View file

@ -24,6 +24,7 @@ import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.events.model.EventType
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity {
val eventEntity = realm.where<EventEntity>() val eventEntity = realm.where<EventEntity>()
@ -31,7 +32,8 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse
.equalTo(EventEntityFields.ROOM_ID, roomId) .equalTo(EventEntityFields.ROOM_ID, roomId)
.findFirst() .findFirst()
return if (eventEntity == null) { 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 this.insertType = insertType
} }
realm.insert(insertEntity) realm.insert(insertEntity)

View file

@ -86,14 +86,17 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr) val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
if (matrixError != null) { if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { when {
matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank() -> {
// Also send this error to the globalErrorReceiver, for a global management // Also send this error to the globalErrorReceiver, for a global management
globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri)) globalErrorReceiver?.handleGlobalError(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ }
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN -> {
// Also send this error to the globalErrorReceiver, for a global management // Also send this error to the globalErrorReceiver, for a global management
globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse())) globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse()))
} }
}
return Failure.ServerError(matrixError, httpCode) return Failure.ServerError(matrixError, httpCode)
} }

View file

@ -77,7 +77,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
val timelineEvent = timelineEventMapper.map(timelineEventEntity) val timelineEvent = timelineEventMapper.map(timelineEventEntity)
timelineInput.onLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent) timelineInput.onLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)
taskExecutor.executorScope.asyncTransaction(monarchy) { realm -> 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 this.insertType = EventInsertType.LOCAL_ECHO
} }
realm.insert(eventInsertEntity) realm.insert(eventInsertEntity)

View file

@ -106,7 +106,8 @@ internal class TimelineEventDecryptor @Inject constructor(
val result = cryptoService.decryptEvent(request.event, timelineId) val result = cryptoService.decryptEvent(request.event, timelineId)
Timber.v("Successfully decrypted event ${event.eventId}") Timber.v("Successfully decrypted event ${event.eventId}")
realm.executeTransaction { realm.executeTransaction {
EventEntity.where(it, eventId = event.eventId ?: "") val eventId = event.eventId ?: ""
EventEntity.where(it, eventId = eventId)
.findFirst() .findFirst()
?.setDecryptionResult(result) ?.setDecryptionResult(result)
} }

View file

@ -143,8 +143,10 @@ android {
resValue "bool", "useLoginV2", "false" resValue "bool", "useLoginV2", "false"
// NotificationSettingsV2 is disabled. To be released in conjunction with iOS/Web // NotificationSettingsV2 is disabled. To be released in conjunction with iOS/Web
resValue "bool", "useNotificationSettingsV1", "true" def useNotificationSettingsV2 = false
resValue "bool", "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" 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' implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.30' implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.31'
// rx // rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'

View file

@ -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.alias.RoomAliasFragment
import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment import im.vector.app.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.app.features.roomprofile.members.RoomMemberListFragment 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.permissions.RoomPermissionsFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleChooseRestrictedFragment import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleChooseRestrictedFragment
@ -717,6 +718,11 @@ interface FragmentModule {
@FragmentKey(RoomBannedMemberListFragment::class) @FragmentKey(RoomBannedMemberListFragment::class)
fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomNotificationSettingsFragment::class)
fun bindRoomNotificationSettingsFragment(fragment: RoomNotificationSettingsFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(SearchFragment::class) @FragmentKey(SearchFragment::class)

View file

@ -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<NotificationSettingsFooterItem.Holder>() {
@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<TextView>(R.id.footerText)
}
}

View file

@ -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<RadioButtonItem.Holder>() {
@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<TextView>(R.id.actionTitle)
val radioImage by bind<ImageView>(R.id.radioIcon)
}
}

View file

@ -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<TextHeaderItem.Holder>() {
@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<TextView>(R.id.headerText)
}
}

View file

@ -65,6 +65,23 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int,
val coloredPart = resources.getString(coloredTextRes) val coloredPart = resources.getString(coloredTextRes)
// Insert colored part into the full text // Insert colored part into the full text
val fullText = resources.getString(fullTextRes, coloredPart) 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 color = ThemeUtils.getColor(context, colorAttribute)
val foregroundSpan = ForegroundColorSpan(color) val foregroundSpan = ForegroundColorSpan(color)

View file

@ -143,6 +143,8 @@ class HomeActivity :
} }
} }
override fun getCoordinatorLayout() = views.coordinatorLayout
override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater) override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater)
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {

View file

@ -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
)

View file

@ -22,15 +22,23 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState 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.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetGenericListBinding import im.vector.app.databinding.BottomSheetGenericListBinding
import im.vector.app.features.navigation.Navigator 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 kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -54,11 +62,13 @@ class RoomListQuickActionsBottomSheet :
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool @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 roomListActionsEpoxyController: RoomListQuickActionsEpoxyController
@Inject lateinit var navigator: Navigator @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 override val showExpanded = true
@ -80,6 +90,12 @@ class RoomListQuickActionsBottomSheet :
disableItemAnimation = true disableItemAnimation = true
) )
roomListActionsEpoxyController.listener = this roomListActionsEpoxyController.listener = this
viewModel.observeViewEvents {
when (it) {
is RoomNotificationSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
}
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -89,7 +105,11 @@ class RoomListQuickActionsBottomSheet :
} }
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
roomListActionsEpoxyController.setData(it) val roomListViewState = RoomListQuickActionViewState(
roomListActionsArgs,
it
)
roomListActionsEpoxyController.setData(roomListViewState)
super.invalidate() super.invalidate()
} }
@ -103,6 +123,10 @@ class RoomListQuickActionsBottomSheet :
} }
} }
override fun didSelectRoomNotificationState(roomNotificationState: RoomNotificationState) {
viewModel.handle(RoomNotificationSettingsAction.SelectNotificationState(roomNotificationState))
}
companion object { companion object {
fun newInstance(roomId: String, mode: RoomListActionsArgs.Mode): RoomListQuickActionsBottomSheet { fun newInstance(roomId: String, mode: RoomListActionsArgs.Mode): RoomListQuickActionsBottomSheet {
return RoomListQuickActionsBottomSheet().apply { 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()
}
} }

View file

@ -15,13 +15,19 @@
*/ */
package im.vector.app.features.home.room.list.actions package im.vector.app.features.home.room.list.actions
import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController 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.bottomSheetDividerItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem 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.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer 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.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -33,16 +39,19 @@ class RoomListQuickActionsEpoxyController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val stringProvider: StringProvider private val stringProvider: StringProvider
) : TypedEpoxyController<RoomListQuickActionsState>() { ) : TypedEpoxyController<RoomListQuickActionViewState>() {
var listener: Listener? = null var listener: Listener? = null
override fun buildModels(state: RoomListQuickActionsState) { override fun buildModels(state: RoomListQuickActionViewState) {
val roomSummary = state.roomSummary() ?: return val notificationViewState = state.notificationSettingsViewState
val roomSummary = notificationViewState.roomSummary() ?: return
val host = this 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 // Preview, favorite, settings
bottomSheetRoomPreviewItem { bottomSheetRoomPreviewItem {
id("room_preview") id("room_preview")
@ -63,17 +72,38 @@ class RoomListQuickActionsEpoxyController @Inject constructor(
} }
} }
val selectedRoomState = state.roomNotificationState() 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.NotificationsAllNoisy(roomSummary.roomId).toBottomSheetItem(0, selectedRoomState)
RoomListQuickActionsSharedAction.NotificationsAll(roomSummary.roomId).toBottomSheetItem(1, selectedRoomState) RoomListQuickActionsSharedAction.NotificationsAll(roomSummary.roomId).toBottomSheetItem(1, selectedRoomState)
RoomListQuickActionsSharedAction.NotificationsMentionsOnly(roomSummary.roomId).toBottomSheetItem(2, selectedRoomState) RoomListQuickActionsSharedAction.NotificationsMentionsOnly(roomSummary.roomId).toBottomSheetItem(2, selectedRoomState)
RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState)
}
if (showAll) { if (showFull) {
RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) 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) { private fun RoomListQuickActionsSharedAction.toBottomSheetItem(index: Int, roomNotificationState: RoomNotificationState? = null) {
val host = this@RoomListQuickActionsEpoxyController val host = this@RoomListQuickActionsEpoxyController
val selected = when (this) { val selected = when (this) {
@ -86,7 +116,11 @@ class RoomListQuickActionsEpoxyController @Inject constructor(
return bottomSheetActionItem { return bottomSheetActionItem {
id("action_$index") id("action_$index")
selected(selected) selected(selected)
if (iconResId != null) {
iconRes(iconResId) iconRes(iconResId)
} else {
showIcon(false)
}
textRes(titleRes) textRes(titleRes)
destructive(this@toBottomSheetItem.destructive) destructive(this@toBottomSheetItem.destructive)
listener { host.listener?.didSelectMenuAction(this@toBottomSheetItem) } listener { host.listener?.didSelectMenuAction(this@toBottomSheetItem) }
@ -95,5 +129,6 @@ class RoomListQuickActionsEpoxyController @Inject constructor(
interface Listener { interface Listener {
fun didSelectMenuAction(quickAction: RoomListQuickActionsSharedAction) fun didSelectMenuAction(quickAction: RoomListQuickActionsSharedAction)
fun didSelectRoomNotificationState(roomNotificationState: RoomNotificationState)
} }
} }

View file

@ -23,7 +23,7 @@ import im.vector.app.core.platform.VectorSharedAction
sealed class RoomListQuickActionsSharedAction( sealed class RoomListQuickActionsSharedAction(
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
@DrawableRes val iconResId: Int, @DrawableRes val iconResId: Int?,
val destructive: Boolean = false) val destructive: Boolean = false)
: VectorSharedAction { : VectorSharedAction {
@ -60,9 +60,9 @@ sealed class RoomListQuickActionsSharedAction(
R.string.room_list_quick_actions_favorite_add, R.string.room_list_quick_actions_favorite_add,
R.drawable.ic_star_24dp) 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.string.room_list_quick_actions_leave,
R.drawable.ic_room_actions_leave, if (showIcon) R.drawable.ic_room_actions_leave else null,
true true
) )
} }

View file

@ -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<RoomSummary> = Uninitialized,
val roomNotificationState: Async<RoomNotificationState> = Uninitialized
) : MvRxState {
constructor(args: RoomListActionsArgs) : this(roomId = args.roomId, mode = args.mode)
}

View file

@ -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<RoomListQuickActionsState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: RoomListQuickActionsState): RoomListQuickActionsViewModel
}
companion object : MvRxViewModelFactory<RoomListQuickActionsViewModel, RoomListQuickActionsState> {
@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
}
}

View file

@ -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.members.RoomMemberListFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsFragment import im.vector.app.features.roomprofile.settings.RoomSettingsFragment
import im.vector.app.features.roomprofile.alias.RoomAliasFragment 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.permissions.RoomPermissionsFragment
import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import javax.inject.Inject import javax.inject.Inject
@ -113,6 +114,7 @@ class RoomProfileActivity :
RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions() RoomProfileSharedAction.OpenRoomPermissionsSettings -> openRoomPermissions()
RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers() RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
RoomProfileSharedAction.OpenRoomNotificationSettings -> openRoomNotificationSettings()
}.exhaustive }.exhaustive
} }
.disposeOnDestroy() .disposeOnDestroy()
@ -162,6 +164,10 @@ class RoomProfileActivity :
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomBannedMemberListFragment::class.java, roomProfileArgs) addFragmentToBackstack(R.id.simpleFragmentContainer, RoomBannedMemberListFragment::class.java, roomProfileArgs)
} }
private fun openRoomNotificationSettings() {
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomNotificationSettingsFragment::class.java, roomProfileArgs)
}
override fun configure(toolbar: MaterialToolbar) { override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar) configureToolbar(toolbar)
} }

View file

@ -30,6 +30,7 @@ import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.AppBarStateChangeListener import im.vector.app.core.animations.AppBarStateChangeListener
import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener
@ -253,10 +254,14 @@ class RoomProfileFragment @Inject constructor(
} }
override fun onNotificationsClicked() { override fun onNotificationsClicked() {
if (BuildConfig.USE_NOTIFICATION_SETTINGS_V2) {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomNotificationSettings)
} else {
RoomListQuickActionsBottomSheet RoomListQuickActionsBottomSheet
.newInstance(roomProfileArgs.roomId, RoomListActionsArgs.Mode.NOTIFICATIONS) .newInstance(roomProfileArgs.roomId, RoomListActionsArgs.Mode.NOTIFICATIONS)
.show(childFragmentManager, "ROOM_PROFILE_NOTIFICATIONS") .show(childFragmentManager, "ROOM_PROFILE_NOTIFICATIONS")
} }
}
override fun onUploadsClicked() { override fun onUploadsClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads) roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads)

View file

@ -28,4 +28,5 @@ sealed class RoomProfileSharedAction : VectorSharedAction {
object OpenRoomUploads : RoomProfileSharedAction() object OpenRoomUploads : RoomProfileSharedAction()
object OpenRoomMembers : RoomProfileSharedAction() object OpenRoomMembers : RoomProfileSharedAction()
object OpenBannedRoomMembers : RoomProfileSharedAction() object OpenBannedRoomMembers : RoomProfileSharedAction()
object OpenRoomNotificationSettings : RoomProfileSharedAction()
} }

View file

@ -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()
}

View file

@ -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<RoomNotificationSettingsViewState>() {
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
}
}

View file

@ -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<FragmentRoomSettingGenericBinding>(),
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)
}
}
}

View file

@ -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()
}

View file

@ -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<RoomNotificationSettingsViewState, RoomNotificationSettingsAction, RoomNotificationSettingsViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: RoomNotificationSettingsViewState): RoomNotificationSettingsViewModel
}
companion object : MvRxViewModelFactory<RoomNotificationSettingsViewModel, RoomNotificationSettingsViewState> {
@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))
}
)
}
}
}

View file

@ -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<RoomSummary> = Uninitialized,
val isLoading: Boolean = false,
val notificationState: Async<RoomNotificationState> = 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<RoomNotificationState>
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<RoomNotificationState>
get() {
return if (roomSummary()?.isEncrypted == true) {
listOf(RoomNotificationState.ALL_MESSAGES_NOISY, RoomNotificationState.MUTE)
} else {
listOf(RoomNotificationState.ALL_MESSAGES_NOISY, RoomNotificationState.MENTIONS_ONLY, RoomNotificationState.MUTE)
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Vector.TextView.Body"
android:id="@+id/footerText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="@dimen/layout_vertical_margin"
android:textColor="?vctr_content_secondary"
tools:text="@string/room_settings_room_notifications_encryption_notice" />

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:minHeight="64dp"
android:foreground="?attr/selectableItemBackground"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingEnd="@dimen/layout_horizontal_margin">
<TextView
android:id="@+id/actionTitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/radioIcon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/room_settings_all_messages" />
<ImageView
android:id="@+id/radioIcon"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@string/a11y_checked"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_radio_on" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/headerText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="@dimen/layout_vertical_margin"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="@dimen/layout_vertical_margin"
android:textColor="?vctr_content_secondary"
android:textStyle="bold"
tools:text="@string/room_settings_room_notifications_notify_me" />

View file

@ -1046,6 +1046,8 @@
<string name="room_settings_all_messages">All messages</string> <string name="room_settings_all_messages">All messages</string>
<string name="room_settings_mention_only">Mentions only</string> <string name="room_settings_mention_only">Mentions only</string>
<string name="room_settings_mute">Mute</string> <string name="room_settings_mute">Mute</string>
<string name="room_settings_mention_and_keyword_only">Mentions &amp; Keywords only</string>
<string name="room_settings_none">None</string>
<string name="room_settings_favourite">Favourite</string> <string name="room_settings_favourite">Favourite</string>
<string name="room_settings_de_prioritize">De-prioritize</string> <string name="room_settings_de_prioritize">De-prioritize</string>
<string name="room_settings_direct_chat">Direct Chat</string> <string name="room_settings_direct_chat">Direct Chat</string>
@ -1441,6 +1443,10 @@
<string name="room_settings_category_access_visibility_title">Access and visibility</string> <string name="room_settings_category_access_visibility_title">Access and visibility</string>
<string name="room_settings_directory_visibility">List this room in room directory</string> <string name="room_settings_directory_visibility">List this room in room directory</string>
<string name="room_settings_room_notifications_title">Notifications</string> <string name="room_settings_room_notifications_title">Notifications</string>
<string name="room_settings_room_notifications_notify_me">Notify me for</string>
<string name="room_settings_room_notifications_encryption_notice">Please note that mentions &amp; keyword notifications are not available in encrypted rooms on mobile.</string>
<string name="room_settings_room_notifications_manage_notifications">You can manage notifications in %1$s.</string>
<string name="room_settings_room_notifications_account_settings">Account settings</string>
<string name="room_settings_room_access_rules_pref_title">Room Access</string> <string name="room_settings_room_access_rules_pref_title">Room Access</string>
<string name="room_settings_room_read_history_rules_pref_title">Room History Readability</string> <string name="room_settings_room_read_history_rules_pref_title">Room History Readability</string>
<string name="room_settings_room_read_history_rules_pref_dialog_title">Who can read history?</string> <string name="room_settings_room_read_history_rules_pref_dialog_title">Who can read history?</string>