mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 02:15:35 +03:00
Merge branch 'develop' into feature/fga/new_voip_design
This commit is contained in:
commit
fd25813862
49 changed files with 1050 additions and 265 deletions
69
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
69
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal 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
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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
36
.github/ISSUE_TEMPLATE/enhancement.yml
vendored
Normal 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
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -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/1823.bugfix
Normal file
1
changelog.d/1823.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
- Add mxid to autocomplete suggestion if more than one user in a room has the same displayname
|
1
changelog.d/3853.feature
Normal file
1
changelog.d/3853.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Call: show dialog for some ended reasons.
|
1
changelog.d/3887.bugfix
Normal file
1
changelog.d/3887.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Message edition is not rendered in e2e rooms after pagination
|
1
changelog.d/pr-3883.misc
Normal file
1
changelog.d/pr-3883.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Issue templates: modernise and sync with element-web
|
|
@ -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'
|
||||
|
|
|
@ -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<EventInsertEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventInsertEntity> {
|
||||
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<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 {
|
||||
return processors.any {
|
||||
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>(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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<EventEntity>()
|
||||
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<RoomMemberSummary>.disambiguate(): Sequence<RoomMemberSummary> {
|
||||
val displayNames = hashMapOf<String, Int>().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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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)
|
||||
}
|
||||
|
|
|
@ -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<CallAudioManager.Device>,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Unit>? = 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>()!!
|
||||
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)
|
||||
|
|
|
@ -143,6 +143,8 @@ class HomeActivity :
|
|||
}
|
||||
}
|
||||
|
||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||
|
||||
override fun getBinding() = ActivityHomeBinding.inflate(layoutInflater)
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<RoomListQuickActionsState>() {
|
||||
) : TypedEpoxyController<RoomListQuickActionViewState>() {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -28,4 +28,5 @@ sealed class RoomProfileSharedAction : VectorSharedAction {
|
|||
object OpenRoomUploads : RoomProfileSharedAction()
|
||||
object OpenRoomMembers : RoomProfileSharedAction()
|
||||
object OpenBannedRoomMembers : RoomProfileSharedAction()
|
||||
object OpenRoomNotificationSettings : RoomProfileSharedAction()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -167,7 +167,6 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/participantNameText"
|
||||
tools:text="@string/call_resume_action" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/callInfoGroup"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -181,16 +180,4 @@
|
|||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/hud_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/call_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
13
vector/src/main/res/layout/item_notifications_footer.xml
Normal file
13
vector/src/main/res/layout/item_notifications_footer.xml
Normal 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" />
|
40
vector/src/main/res/layout/item_radio.xml
Normal file
40
vector/src/main/res/layout/item_radio.xml
Normal 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>
|
14
vector/src/main/res/layout/item_text_header.xml
Normal file
14
vector/src/main/res/layout/item_text_header.xml
Normal 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" />
|
|
@ -752,6 +752,9 @@
|
|||
<string name="call_held_by_user">%s held the call</string>
|
||||
<string name="call_held_by_you">You held the call</string>
|
||||
|
||||
<string name="call_ended_user_busy_title">User busy</string>
|
||||
<string name="call_ended_user_busy_description">The user you called is busy."</string>
|
||||
<string name="call_ended_invite_timeout_title">No answer</string>
|
||||
<string name="call_error_user_not_responding">The remote side failed to pick up.</string>
|
||||
<string name="call_error_ice_failed">Media Connection Failed</string>
|
||||
<string name="call_error_camera_init_failed">Cannot initialize the camera</string>
|
||||
|
@ -1051,6 +1054,8 @@
|
|||
<string name="room_settings_all_messages">All messages</string>
|
||||
<string name="room_settings_mention_only">Mentions only</string>
|
||||
<string name="room_settings_mute">Mute</string>
|
||||
<string name="room_settings_mention_and_keyword_only">Mentions & Keywords only</string>
|
||||
<string name="room_settings_none">None</string>
|
||||
<string name="room_settings_favourite">Favourite</string>
|
||||
<string name="room_settings_de_prioritize">De-prioritize</string>
|
||||
<string name="room_settings_direct_chat">Direct Chat</string>
|
||||
|
@ -1441,6 +1446,10 @@
|
|||
<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_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 & 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_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>
|
||||
|
|
Loading…
Reference in a new issue