Added banned user screen

This commit is contained in:
Valere 2020-07-28 10:09:39 +02:00
parent de32cdb703
commit c82e910c38
14 changed files with 552 additions and 4 deletions

View File

@ -8,6 +8,7 @@ Improvements 🙌:
- Sending events is now retried only 3 times, so we avoid blocking the sending queue too long.
- Display warning when fail to send events in room list
- Improve UI of edit role action in member profile
- Moderation | New screen to display list of banned users in room settings, with unban action
Bugfix 🐛:
- Fix theme issue on Room directory screen (#1613)

View File

@ -77,6 +77,7 @@ import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment
import im.vector.riotx.features.roommemberprofile.devices.DeviceListFragment
import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionFragment
import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
@ -534,4 +535,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(ContactsBookFragment::class)
fun bindPhoneBookFragment(fragment: ContactsBookFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomBannedMemberListFragment::class)
fun bindRoomBannedMemberListFragment(fragment: RoomBannedMemberListFragment): Fragment
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2020 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.riotx.core.epoxy.profiles
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.crypto.util.toImageRes
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_profile_matrix_item_progress)
abstract class ProfileMatrixItemProgress : VectorEpoxyModel<ProfileMatrixItemProgress.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
val bestName = matrixItem.getBestName()
val matrixId = matrixItem.id
.takeIf { it != bestName }
// Special case for ThreePid fake matrix item
.takeIf { it != "@" }
holder.titleView.text = bestName
holder.subtitleView.setTextOrHide(matrixId)
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.avatarDecorationImageView.setImageResource(userEncryptionTrustLevel.toImageRes())
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.matrixItemTitle)
val subtitleView by bind<TextView>(R.id.matrixItemSubtitle)
val avatarImageView by bind<ImageView>(R.id.matrixItemAvatar)
val avatarDecorationImageView by bind<ImageView>(R.id.matrixItemAvatarDecoration)
}
}

View File

@ -32,6 +32,7 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.room.RequireActiveMembershipViewEvents
import im.vector.riotx.features.room.RequireActiveMembershipViewModel
import im.vector.riotx.features.room.RequireActiveMembershipViewState
import im.vector.riotx.features.roomprofile.banned.RoomBannedMemberListFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
@ -84,6 +85,7 @@ class RoomProfileActivity :
is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers()
is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings()
is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads()
is RoomProfileSharedAction.OpenBannedRoomMembers -> openBannedRoomMembers()
}
}
.disposeOnDestroy()
@ -114,6 +116,10 @@ class RoomProfileActivity :
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs)
}
private fun openBannedRoomMembers() {
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomBannedMemberListFragment::class.java, roomProfileArgs)
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}

View File

@ -41,6 +41,7 @@ class RoomProfileController @Inject constructor(
interface Callback {
fun onLearnMoreClicked()
fun onMemberListClicked()
fun onBannedMemberListClicked()
fun onNotificationsClicked()
fun onUploadsClicked()
fun onSettingsClicked()
@ -92,6 +93,16 @@ class RoomProfileController @Inject constructor(
accessory = R.drawable.ic_shield_warning.takeIf { hasWarning } ?: 0,
action = { callback?.onMemberListClicked() }
)
if (data.bannedMembership.invoke()?.isNotEmpty() == true) {
buildProfileAction(
id = "banned_list",
title = stringProvider.getString(R.string.room_settings_banned_users_title),
dividerColor = dividerColor,
icon = R.drawable.ic_settings_root_labs,
action = { callback?.onBannedMemberListClicked() }
)
}
buildProfileAction(
id = "uploads",
title = stringProvider.getString(R.string.room_profile_section_more_uploads),

View File

@ -206,6 +206,10 @@ class RoomProfileFragment @Inject constructor(
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers)
}
override fun onBannedMemberListClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenBannedRoomMembers)
}
override fun onSettingsClicked() {
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomSettings)
}

View File

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

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.roomprofile
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -26,6 +27,8 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
@ -61,7 +64,8 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
val rxRoom = room.rx()
rxRoom.liveRoomSummary()
.unwrap()
.execute {
copy(roomSummary = it)
@ -77,6 +81,16 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted private val ini
}
}
.disposeOnClear()
rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) })
.subscribe {
setState {
copy(
bannedMembership = Success(it)
)
}
}
.disposeOnClear()
}
override fun handle(action: RoomProfileAction) = when (action) {

View File

@ -20,11 +20,13 @@ package im.vector.riotx.features.roomprofile
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
data class RoomProfileViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMembership: Async<List<RoomMemberSummary>> = Uninitialized,
val canChangeAvatar: Boolean = false
) : MvRxState {

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2020 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.riotx.features.roomprofile.banned
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.profiles.buildProfileSection
import im.vector.riotx.core.epoxy.profiles.profileMatrixItem
import im.vector.riotx.core.epoxy.profiles.profileMatrixItemProgress
import im.vector.riotx.core.extensions.join
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class RoomBannedMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomBannedMemberListViewState>() {
interface Callback {
fun onUnbanClicked(roomMember: RoomMemberSummary)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var callback: Callback? = null
init {
setData(null)
}
override fun buildModels(data: RoomBannedMemberListViewState?) {
val bannedList = data?.bannedMemberSummaries?.invoke() ?: return
buildProfileSection(
stringProvider.getString(R.string.room_settings_banned_users_title)
)
bannedList.join(
each = { _, roomMember ->
if (data.onGoingModerationAction.contains(roomMember.userId)) {
profileMatrixItemProgress {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer)
}
} else {
profileMatrixItem {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onUnbanClicked(roomMember)
}
}
}
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
color(dividerColor)
}
}
)
genericFooterItem {
id("footer")
text(stringProvider.getQuantityString(R.plurals.room_settings_banned_users_count, bannedList.size, bannedList.size))
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) 2020 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.riotx.features.roomprofile.banned
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.roomprofile.RoomProfileArgs
import kotlinx.android.synthetic.main.fragment_room_setting_generic.*
import javax.inject.Inject
class RoomBannedMemberListFragment @Inject constructor(
val viewModelFactory: RoomBannedListMemberViewModel.Factory,
private val roomMemberListController: RoomBannedMemberListController,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment(), RoomBannedMemberListController.Callback {
private val viewModel: RoomBannedListMemberViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args()
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
override fun onUnbanClicked(roomMember: RoomMemberSummary) {
viewModel.handle(RoomBannedListMemberAction.QueryInfo(roomMember))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar)
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.observeViewEvents {
when (it) {
is RoomBannedViewEvents.ShowBannedInfo -> {
val canBan = withState(viewModel) { state -> state.canUserBan }
AlertDialog.Builder(requireActivity())
.setTitle(getString(R.string.member_banned_by, it.bannedByUserId))
.setMessage(getString(R.string.reason_colon, it.banReason))
.setPositiveButton(R.string.ok, null)
.apply {
if (canBan) {
setNegativeButton(R.string.room_participants_action_unban) { _, _ ->
viewModel.handle(RoomBannedListMemberAction.UnBanUser(it.roomMemberSummary))
}
}
}
.show()
}
is RoomBannedViewEvents.ToastError -> {
requireActivity().toast(it.info)
}
}
}
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { viewState ->
roomMemberListController.setData(viewState)
renderRoomSummary(viewState)
}
private fun renderRoomSummary(state: RoomBannedMemberListViewState) {
state.roomSummary()?.let {
roomSettingsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), roomSettingsToolbarAvatarImageView)
}
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright (c) 2020 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.riotx.features.roomprofile.banned
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
import im.vector.riotx.features.roomprofile.RoomProfileArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
data class RoomBannedMemberListViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val bannedMemberSummaries: Async<List<RoomMemberSummary>> = Uninitialized,
val onGoingModerationAction: List<String> = emptyList(),
val canUserBan: Boolean = false
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}
sealed class RoomBannedListMemberAction : VectorViewModelAction {
data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction()
}
sealed class RoomBannedViewEvents : VectorViewEvents {
data class ShowBannedInfo(val bannedByUserId: String, val banReason: String, val roomMemberSummary: RoomMemberSummary) : RoomBannedViewEvents()
data class ToastError(val info: String) : RoomBannedViewEvents()
}
class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initialState: RoomBannedMemberListViewState,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<RoomBannedMemberListViewState, RoomBannedListMemberAction, RoomBannedViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomBannedMemberListViewState): RoomBannedListMemberViewModel
}
private val room = session.getRoom(initialState.roomId)!!
init {
val rxRoom = room.rx()
room.rx().liveRoomSummary()
.unwrap()
.execute { async ->
copy(roomSummary = async)
}
rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) })
.execute {
copy(
bannedMemberSummaries = it
)
}
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
powerLevelsContentLive.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
setState { copy(canUserBan = powerLevelsHelper.isUserAbleToBan(session.myUserId)) }
}.disposeOnClear()
}
companion object : MvRxViewModelFactory<RoomBannedListMemberViewModel, RoomBannedMemberListViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomBannedMemberListViewState): RoomBannedListMemberViewModel? {
val fragment: RoomBannedMemberListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
override fun handle(action: RoomBannedListMemberAction) {
when (action) {
is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary)
is RoomBannedListMemberAction.UnBanUser -> unBanUser(action.roomMemberSummary)
}
}
private fun onQueryBanInfo(roomMemberSummary: RoomMemberSummary) {
val bannedEvent = room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(roomMemberSummary.userId))
val content = bannedEvent?.getClearContent().toModel<RoomMemberContent>()
if (content?.membership != Membership.BAN) {
// may be report error?
return
}
val reason = content.reason
val bannedBy = bannedEvent?.senderId ?: return
_viewEvents.post(RoomBannedViewEvents.ShowBannedInfo(bannedBy, reason ?: "", roomMemberSummary))
}
private fun unBanUser(roomMemberSummary: RoomMemberSummary) {
setState {
copy(onGoingModerationAction = this.onGoingModerationAction + roomMemberSummary.userId)
}
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback<Unit> {
room.unban(roomMemberSummary.userId, null, it)
}
} catch (failure: Throwable) {
_viewEvents.post(RoomBannedViewEvents.ToastError(stringProvider.getString(R.string.failed_to_unban)))
} finally {
setState {
copy(
onGoingModerationAction = onGoingModerationAction - roomMemberSummary.userId
)
}
}
}
}
}

View File

@ -0,0 +1,88 @@
<?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:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="64dp"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/matrixItemAvatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/matrixItemAvatarDecoration"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintCircle="@+id/matrixItemAvatar"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="16dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<TextView
android:id="@+id/matrixItemTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/matrixItemSubtitle"
app:layout_constraintEnd_toStartOf="@+id/matrixItemProgress"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/matrixItemSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:drawablePadding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/matrixItemProgress"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/matrixItemAvatar"
app:layout_constraintTop_toBottomOf="@id/matrixItemTitle"
app:layout_goneMarginStart="0dp"
tools:text="@sample/matrix.json/data/mxid" />
<ProgressBar
android:id="@+id/matrixItemProgress"
style="?android:attr/progressBarStyle"
android:layout_width="20dp"
android:layout_height="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -992,6 +992,10 @@
<!-- Room settings: banned users -->
<string name="room_settings_banned_users_title">Banned users</string>
<plurals name="room_settings_banned_users_count">
<item quantity="one">1 banned user</item>
<item quantity="other">%d banned users</item>
</plurals>
<!-- advanced -->
<string name="room_settings_category_advanced_title">Advanced</string>
@ -2561,4 +2565,7 @@ Not all features in Riot are implemented in Element yet. Main missing (and comin
<string name="three_pid_revoke_invite_dialog_title">Revoke invite</string>
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>
<string name="member_banned_by">Banned by %1$s</string>
<string name="failed_to_unban">Failed to UnBan user</string>
</resources>