Power levels: handle some action permissions

This commit is contained in:
ganfra 2020-06-05 19:12:33 +02:00
parent a1fd35aa67
commit e5da5a34cb
21 changed files with 465 additions and 272 deletions

View File

@ -51,6 +51,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
/**
* Tell if an user can send an event of a certain type
*
* @param isState true if the event is a state event (ie. state key is not null)
* @param eventType the event type to check for
* @param userId the user id
* @return true if the user can send this type of event
@ -68,6 +69,26 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
} else false
}
fun canInvite(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.invite
}
fun canBan(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.ban
}
fun canKick(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.kick
}
fun canRedact(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redact
}
/**
* Get the notification level for a dedicated key.
*

View File

@ -34,7 +34,9 @@ import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.R
import im.vector.riotx.core.error.ResourceLimitErrorFormatter
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.themes.ThemeUtils
import kotlinx.android.synthetic.main.view_notification_area.view.*
import me.gujun.android.span.span
import me.saket.bettermovementmethod.BetterLinkMovementMethod
import timber.log.Timber
@ -49,11 +51,6 @@ class NotificationAreaView @JvmOverloads constructor(
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
@BindView(R.id.room_notification_icon)
lateinit var imageView: ImageView
@BindView(R.id.room_notification_message)
lateinit var messageView: TextView
var delegate: Delegate? = null
private var state: State = State.Initial
@ -77,6 +74,7 @@ class NotificationAreaView @JvmOverloads constructor(
when (newState) {
is State.Default -> renderDefault()
is State.Hidden -> renderHidden()
is State.NoPermissionToPost -> renderNoPermissionToPost()
is State.Tombstone -> renderTombstone(newState)
is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState)
is State.ConnectionError -> renderConnectionError()
@ -91,20 +89,30 @@ class NotificationAreaView @JvmOverloads constructor(
private fun setupView() {
inflate(context, R.layout.view_notification_area, this)
ButterKnife.bind(this)
minimumHeight = DimensionConverter(resources).dpToPx(48)
}
private fun cleanUp() {
messageView.setOnClickListener(null)
imageView.setOnClickListener(null)
roomNotificationMessage.setOnClickListener(null)
roomNotificationIcon.setOnClickListener(null)
setBackgroundColor(Color.TRANSPARENT)
messageView.text = null
imageView.setImageResource(0)
roomNotificationMessage.text = null
roomNotificationIcon.setImageResource(0)
}
private fun renderNoPermissionToPost() {
visibility = View.VISIBLE
roomNotificationIcon.setImageDrawable(null)
val message = span {
+resources.getString(R.string.room_do_not_have_permission_to_post)
}
roomNotificationMessage.text = message
roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.riotx_text_secondary))
}
private fun renderTombstone(state: State.Tombstone) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
roomNotificationIcon.setImageResource(R.drawable.error)
val message = span {
+resources.getString(R.string.room_tombstone_versioned_description)
+"\n"
@ -113,8 +121,8 @@ class NotificationAreaView @JvmOverloads constructor(
onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) }
}
}
messageView.movementMethod = BetterLinkMovementMethod.getInstance()
messageView.text = message
roomNotificationMessage.movementMethod = BetterLinkMovementMethod.getInstance()
roomNotificationMessage.text = message
}
private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) {
@ -130,54 +138,54 @@ class NotificationAreaView @JvmOverloads constructor(
formatterMode = ResourceLimitErrorFormatter.Mode.Hard
}
val message = resourceLimitErrorFormatter.format(state.matrixError, formatterMode, clickable = true)
messageView.setTextColor(Color.WHITE)
messageView.text = message
messageView.movementMethod = LinkMovementMethod.getInstance()
messageView.setLinkTextColor(Color.WHITE)
roomNotificationMessage.setTextColor(Color.WHITE)
roomNotificationMessage.text = message
roomNotificationMessage.movementMethod = LinkMovementMethod.getInstance()
roomNotificationMessage.setLinkTextColor(Color.WHITE)
setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
}
private fun renderConnectionError() {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
messageView.text = SpannableString(resources.getString(R.string.room_offline_notification))
roomNotificationIcon.setImageResource(R.drawable.error)
roomNotificationMessage.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
roomNotificationMessage.text = SpannableString(resources.getString(R.string.room_offline_notification))
}
private fun renderTyping(state: State.Typing) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.vector_typing)
messageView.text = SpannableString(state.message)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
roomNotificationIcon.setImageResource(R.drawable.vector_typing)
roomNotificationMessage.text = SpannableString(state.message)
roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
}
private fun renderUnreadPreview() {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.scrolldown)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
imageView.setOnClickListener { delegate?.closeScreen() }
roomNotificationIcon.setImageResource(R.drawable.scrolldown)
roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
roomNotificationIcon.setOnClickListener { delegate?.closeScreen() }
}
private fun renderScrollToBottom(state: State.ScrollToBottom) {
visibility = View.VISIBLE
if (state.unreadCount > 0) {
imageView.setImageResource(R.drawable.newmessages)
messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
messageView.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount))
roomNotificationIcon.setImageResource(R.drawable.newmessages)
roomNotificationMessage.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
roomNotificationMessage.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount))
} else {
imageView.setImageResource(R.drawable.scrolldown)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
roomNotificationIcon.setImageResource(R.drawable.scrolldown)
roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
if (!state.message.isNullOrEmpty()) {
messageView.text = SpannableString(state.message)
roomNotificationMessage.text = SpannableString(state.message)
}
}
messageView.setOnClickListener { delegate?.jumpToBottom() }
imageView.setOnClickListener { delegate?.jumpToBottom() }
roomNotificationMessage.setOnClickListener { delegate?.jumpToBottom() }
roomNotificationIcon.setOnClickListener { delegate?.jumpToBottom() }
}
private fun renderUnsent(state: State.UnsentEvents) {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
roomNotificationIcon.setImageResource(R.drawable.error)
val cancelAll = resources.getString(R.string.room_prompt_cancel)
val resendAll = resources.getString(R.string.room_prompt_resend)
val messageRes = if (state.hasUnknownDeviceEvents) R.string.room_unknown_devices_messages_notification else R.string.room_unsent_messages_notification
@ -194,9 +202,9 @@ class NotificationAreaView @JvmOverloads constructor(
if (resendAllPos >= 0) {
spannableString.setSpan(ResendAllClickableSpan(), resendAllPos, resendAllPos + resendAll.length, 0)
}
messageView.movementMethod = LinkMovementMethod.getInstance()
messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
messageView.text = spannableString
roomNotificationMessage.movementMethod = LinkMovementMethod.getInstance()
roomNotificationMessage.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
roomNotificationMessage.text = spannableString
}
private fun renderDefault() {
@ -254,6 +262,8 @@ class NotificationAreaView @JvmOverloads constructor(
// View will be Invisible
object Default : State()
object NoPermissionToPost: State()
// View will be Gone
object Hidden : State()
@ -289,26 +299,4 @@ class NotificationAreaView @JvmOverloads constructor(
fun closeScreen()
fun jumpToBottom()
}
companion object {
/**
* Preference key.
*/
private const val SHOW_INFO_AREA_KEY = "SETTINGS_SHOW_INFO_AREA_KEY"
/**
* Always show the info area.
*/
private const val SHOW_INFO_AREA_VALUE_ALWAYS = "always"
/**
* Show the info area when it has messages or errors.
*/
private const val SHOW_INFO_AREA_VALUE_MESSAGES_AND_ERRORS = "messages_and_errors"
/**
* Show the info area only when it has errors.
*/
private const val SHOW_INFO_AREA_VALUE_ONLY_ERRORS = "only_errors"
}
}

View File

@ -285,7 +285,10 @@ class RoomDetailFragment @Inject constructor(
renderTombstoneEventHandling(it)
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode, RoomDetailViewState::canSendMessage) { mode, canSend ->
if (!canSend) {
return@selectSubscribe
}
when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text)
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
@ -372,6 +375,7 @@ class RoomDetailFragment @Inject constructor(
modelBuildListener = null
debouncer.cancelAll()
recyclerView.cleanup()
super.onDestroyView()
}
@ -611,6 +615,12 @@ class RoomDetailFragment @Inject constructor(
}
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
val canSendMessage = withState(roomDetailViewModel) {
it.canSendMessage
}
if (!canSendMessage) {
return false
}
return when (model) {
is MessageFileItem,
is MessageImageVideoItem,
@ -733,24 +743,27 @@ class RoomDetailFragment @Inject constructor(
val uid = session.myUserId
val meMember = state.myRoomMember()
avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView)
if (state.tombstoneEvent == null) {
if (state.canSendMessage) {
composerLayout.visibility = View.VISIBLE
composerLayout.setRoomEncrypted(summary.isEncrypted, summary.roomEncryptionTrustLevel)
notificationAreaView.render(NotificationAreaView.State.Hidden)
} else {
composerLayout.visibility = View.GONE
notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost)
}
} else {
composerLayout.visibility = View.GONE
notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
}
} else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
// Intercept click event
inviteView.setOnClickListener { }
} else if (state.asyncInviter.complete) {
vectorBaseActivity.finish()
}
val isRoomEncrypted = summary?.isEncrypted ?: false
if (state.tombstoneEvent == null) {
composerLayout.visibility = View.VISIBLE
composerLayout.setRoomEncrypted(isRoomEncrypted, state.asyncRoomSummary.invoke()?.roomEncryptionTrustLevel)
notificationAreaView.render(NotificationAreaView.State.Hidden)
} else {
composerLayout.visibility = View.GONE
notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent))
}
}
private fun renderRoomSummary(state: RoomDetailViewState) {
@ -1378,7 +1391,9 @@ class RoomDetailFragment @Inject constructor(
}
private fun focusComposerAndShowKeyboard() {
composerLayout.composerEditText.showKeyboard(andRequestFocus = true)
if (composerLayout.isVisible) {
composerLayout.composerEditText.showKeyboard(andRequestFocus = true)
}
}
private fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {

View File

@ -48,6 +48,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.OptionItem
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.Timeline
@ -71,6 +72,7 @@ import im.vector.riotx.features.crypto.verification.SupportedVerificationMethods
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.riotx.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
import im.vector.riotx.features.home.room.typing.TypingHelper
import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
@ -163,6 +165,7 @@ class RoomDetailViewModel @AssistedInject constructor(
observeUnreadState()
observeMyRoomMember()
observeActiveRoomWidgets()
observePowerLevel()
room.getRoomSummaryLive()
room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback())
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
@ -170,6 +173,16 @@ class RoomDetailViewModel @AssistedInject constructor(
session.onRoomDisplayed(initialState.roomId)
}
private fun observePowerLevel() {
PowerLevelsObservableFactory(room).createObservable()
.subscribe {
val canSendMessage = PowerLevelsHelper(it).isAllowedToSend(false, EventType.MESSAGE, session.myUserId)
setState {
copy(canSendMessage = canSendMessage)
}
}.disposeOnClear()
}
private fun observeActiveRoomWidgets() {
session.rx()
.liveRoomWidgets(

View File

@ -25,8 +25,8 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.MatrixItem
/**
* Describes the current send mode:
@ -65,7 +65,8 @@ data class RoomDetailViewState(
val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true
val canShowJumpToReadMarker: Boolean = true,
val canSendMessage: Boolean = false
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -0,0 +1,66 @@
/*
* 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.home.room.detail.timeline.action
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Quick reactions state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class ActionPermissions(
val canSendMessage: Boolean = false,
val canReact: Boolean = false,
val canRedact: Boolean = false
)
data class MessageActionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val timelineEvent: Async<TimelineEvent> = Uninitialized,
val messageBody: CharSequence = "",
// For quick reactions
val quickStates: Async<List<ToggleState>> = Uninitialized,
// For actions
val actions: List<EventSharedAction> = emptyList(),
val expendedReportContentMenu: Boolean = false,
val actionPermissions: ActionPermissions = ActionPermissions()
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
}

View File

@ -15,11 +15,8 @@
*/
package im.vector.riotx.features.home.room.detail.timeline.action
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
@ -35,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -47,46 +45,11 @@ import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.settings.VectorPreferences
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Quick reactions state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class MessageActionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val timelineEvent: Async<TimelineEvent> = Uninitialized,
val messageBody: CharSequence = "",
// For quick reactions
val quickStates: Async<List<ToggleState>> = Uninitialized,
// For actions
val actions: List<EventSharedAction> = emptyList(),
val expendedReportContentMenu: Boolean = false
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
fun canReact() = timelineEvent()?.canReact() == true
}
/**
* Information related to an event and used to display preview in contextual bottom sheet.
@ -121,6 +84,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
init {
observeEvent()
observeReactions()
observePowerLevel()
observeTimelineEventState()
}
@ -138,6 +102,23 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
}
private fun observePowerLevel() {
if (room == null) {
return
}
PowerLevelsObservableFactory(room).createObservable()
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
val canReact = powerLevelsHelper.isAllowedToSend(false, EventType.REACTION, session.myUserId)
val canRedact = powerLevelsHelper.canRedact(session.myUserId)
val canSendMessage = powerLevelsHelper.isAllowedToSend(false, EventType.MESSAGE, session.myUserId)
val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact)
setState {
copy(actionPermissions = permissions)
}
}.disposeOnClear()
}
private fun observeEvent() {
if (room == null) return
room.rx()
@ -163,11 +144,12 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
private fun observeTimelineEventState() {
asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent ->
selectSubscribe(MessageActionState::timelineEvent, MessageActionState::actionPermissions) { timelineEvent, permissions ->
val nonNullTimelineEvent = timelineEvent() ?: return@selectSubscribe
setState {
copy(
messageBody = computeMessageBody(timelineEvent),
actions = actionsForEvent(timelineEvent)
messageBody = computeMessageBody(nonNullTimelineEvent),
actions = actionsForEvent(nonNullTimelineEvent, permissions)
)
}
}
@ -235,14 +217,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
}
private fun actionsForEvent(timelineEvent: TimelineEvent): List<EventSharedAction> {
private fun actionsForEvent(timelineEvent: TimelineEvent, actionPermissions: ActionPermissions): List<EventSharedAction> {
val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: timelineEvent.root.getClearContent().toModel()
val msgType = messageContent?.msgType
return arrayListOf<EventSharedAction>().apply {
if (timelineEvent.root.sendState.hasFailed()) {
if (canRetry(timelineEvent)) {
if (canRetry(timelineEvent, actionPermissions)) {
add(EventSharedAction.Resend(eventId))
}
add(EventSharedAction.Remove(eventId))
@ -253,15 +235,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
} else if (timelineEvent.root.sendState == SendState.SYNCED) {
if (!timelineEvent.root.isRedacted()) {
if (canReply(timelineEvent, messageContent)) {
if (canReply(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Reply(eventId))
}
if (canEdit(timelineEvent, session.myUserId)) {
if (canEdit(timelineEvent, session.myUserId, actionPermissions)) {
add(EventSharedAction.Edit(eventId))
}
if (canRedact(timelineEvent, session.myUserId)) {
if (canRedact(timelineEvent, actionPermissions)) {
add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId))
}
@ -270,11 +252,11 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.Copy(messageContent!!.body))
}
if (timelineEvent.canReact()) {
if (timelineEvent.canReact() && actionPermissions.canReact) {
add(EventSharedAction.AddReaction(eventId))
}
if (canQuote(timelineEvent, messageContent)) {
if (canQuote(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.Quote(eventId))
}
@ -340,9 +322,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
return false
}
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
@ -355,9 +338,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
}
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
if(!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
@ -369,15 +353,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.root.senderId == myUserId
return actionPermissions.canRedact
}
private fun canRetry(event: TimelineEvent): Boolean {
return event.root.sendState.hasFailed() && event.root.isTextMessage()
private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
return event.root.sendState.hasFailed() && event.root.isTextMessage() && actionPermissions.canSendMessage
}
private fun canViewReactions(event: TimelineEvent): Boolean {
@ -387,9 +370,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
if(!actionPermissions.canSendMessage) return false
// TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>()
return event.root.senderId == myUserId && (

View File

@ -0,0 +1,39 @@
/*
* 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.powerlevel
import im.vector.matrix.android.api.query.QueryStringValue
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.Room
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.rx.mapOptional
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
class PowerLevelsObservableFactory(private val room: Room) {
fun createObservable(): Observable<PowerLevelsContent> {
return room.rx()
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
.observeOn(Schedulers.computation())
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
}
}

View File

@ -47,7 +47,7 @@ class RoomMemberProfileController @Inject constructor(
fun onShowDeviceListNoCrossSigning()
fun onJumpToReadReceiptClicked()
fun onMentionClicked()
fun onSetPowerLevel(userRole: Role)
fun onEditPowerLevel(currentRole: Role)
fun onKickClicked()
fun onBanClicked(isUserBanned: Boolean)
fun onCancelInviteClicked()
@ -86,108 +86,6 @@ class RoomMemberProfileController @Inject constructor(
buildAdminSection(state)
}
private fun buildAdminSection(state: RoomMemberProfileViewState) {
val powerLevelsContent = state.powerLevelsContent() ?: return
val powerLevelsStr = state.userPowerLevelString() ?: return
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
val userPowerLevel = powerLevelsHelper.getUserRole(state.userId)
val myPowerLevel = powerLevelsHelper.getUserRole(session.myUserId)
if (myPowerLevel < Role.Moderator || (!state.isMine && myPowerLevel <= userPowerLevel)) {
return
}
val membership = state.asyncMembership() ?: return
buildProfileSection(stringProvider.getString(R.string.room_profile_section_admin))
buildProfileAction(
id = "set_power_level",
editable = false,
title = powerLevelsStr,
dividerColor = dividerColor,
action = { callback?.onSetPowerLevel(userPowerLevel) }
)
if (membership == Membership.JOIN) {
buildProfileAction(
id = "kick",
editable = false,
destructive = true,
title = stringProvider.getString(R.string.room_participants_action_kick),
dividerColor = dividerColor,
action = { callback?.onKickClicked() }
)
} else if (membership == Membership.INVITE) {
buildProfileAction(
id = "cancel_invite",
title = stringProvider.getString(R.string.room_participants_action_cancel_invite),
dividerColor = dividerColor,
destructive = true,
editable = false,
action = { callback?.onCancelInviteClicked() }
)
}
val banActionTitle = if (membership == Membership.BAN) {
stringProvider.getString(R.string.room_participants_action_unban)
} else {
stringProvider.getString(R.string.room_participants_action_ban)
}
buildProfileAction(
id = "ban",
editable = false,
destructive = true,
title = banActionTitle,
dividerColor = dividerColor,
action = { callback?.onBanClicked(membership == Membership.BAN) }
)
}
private fun buildMoreSection(state: RoomMemberProfileViewState) {
// More
if (!state.isMine) {
val membership = state.asyncMembership() ?: return
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
id = "read_receipt",
editable = false,
title = stringProvider.getString(R.string.room_member_jump_to_read_receipt),
dividerColor = dividerColor,
action = { callback?.onJumpToReadReceiptClicked() }
)
val ignoreActionTitle = state.buildIgnoreActionTitle()
buildProfileAction(
id = "mention",
title = stringProvider.getString(R.string.room_participants_action_mention),
dividerColor = dividerColor,
editable = false,
divider = ignoreActionTitle != null,
action = { callback?.onMentionClicked() }
)
if (membership == Membership.LEAVE || membership == Membership.KNOCK) {
buildProfileAction(
id = "invite",
title = stringProvider.getString(R.string.room_participants_action_invite),
dividerColor = dividerColor,
destructive = false,
editable = false,
divider = ignoreActionTitle != null,
action = { callback?.onInviteClicked() }
)
}
if (ignoreActionTitle != null) {
buildProfileAction(
id = "ignore",
title = ignoreActionTitle,
dividerColor = dividerColor,
destructive = true,
editable = false,
divider = false,
action = { callback?.onIgnoreClicked() }
)
}
}
}
private fun buildSecuritySection(state: RoomMemberProfileViewState) {
// Security
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
@ -268,6 +166,124 @@ class RoomMemberProfileController @Inject constructor(
}
}
private fun buildMoreSection(state: RoomMemberProfileViewState) {
// More
if (!state.isMine) {
val membership = state.asyncMembership() ?: return
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
id = "read_receipt",
editable = false,
title = stringProvider.getString(R.string.room_member_jump_to_read_receipt),
dividerColor = dividerColor,
action = { callback?.onJumpToReadReceiptClicked() }
)
val ignoreActionTitle = state.buildIgnoreActionTitle()
buildProfileAction(
id = "mention",
title = stringProvider.getString(R.string.room_participants_action_mention),
dividerColor = dividerColor,
editable = false,
divider = ignoreActionTitle != null,
action = { callback?.onMentionClicked() }
)
val canInvite = state.actionPermissions.canInvite
if (canInvite && (membership == Membership.LEAVE || membership == Membership.KNOCK)) {
buildProfileAction(
id = "invite",
title = stringProvider.getString(R.string.room_participants_action_invite),
dividerColor = dividerColor,
destructive = false,
editable = false,
divider = ignoreActionTitle != null,
action = { callback?.onInviteClicked() }
)
}
if (ignoreActionTitle != null) {
buildProfileAction(
id = "ignore",
title = ignoreActionTitle,
dividerColor = dividerColor,
destructive = true,
editable = false,
divider = false,
action = { callback?.onIgnoreClicked() }
)
}
}
}
private fun buildAdminSection(state: RoomMemberProfileViewState) {
val powerLevelsContent = state.powerLevelsContent ?: return
val powerLevelsStr = state.userPowerLevelString() ?: return
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
val userPowerLevel = powerLevelsHelper.getUserRole(state.userId)
val myPowerLevel = powerLevelsHelper.getUserRole(session.myUserId)
if ((!state.isMine && myPowerLevel <= userPowerLevel)) {
return
}
val membership = state.asyncMembership() ?: return
val canKick = state.actionPermissions.canKick
val canBan = state.actionPermissions.canBan
val canEditPowerLevel = state.actionPermissions.canEditPowerLevel
if (canKick || canBan || canEditPowerLevel) {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_admin))
}
if (canEditPowerLevel) {
buildProfileAction(
id = "edit_power_level",
editable = false,
title = powerLevelsStr,
divider = canKick || canBan,
dividerColor = dividerColor,
action = { callback?.onEditPowerLevel(userPowerLevel) }
)
}
if (canKick) {
if (membership == Membership.JOIN) {
buildProfileAction(
id = "kick",
editable = false,
divider = canBan,
destructive = true,
title = stringProvider.getString(R.string.room_participants_action_kick),
dividerColor = dividerColor,
action = { callback?.onKickClicked() }
)
} else if (membership == Membership.INVITE) {
buildProfileAction(
id = "cancel_invite",
title = stringProvider.getString(R.string.room_participants_action_cancel_invite),
divider = canBan,
dividerColor = dividerColor,
destructive = true,
editable = false,
action = { callback?.onCancelInviteClicked() }
)
}
}
if (canBan) {
val banActionTitle = if (membership == Membership.BAN) {
stringProvider.getString(R.string.room_participants_action_unban)
} else {
stringProvider.getString(R.string.room_participants_action_ban)
}
buildProfileAction(
id = "ban",
editable = false,
destructive = true,
title = banActionTitle,
dividerColor = dividerColor,
action = { callback?.onBanClicked(membership == Membership.BAN) }
)
}
}
private fun RoomMemberProfileViewState.buildIgnoreActionTitle(): String? {
val isIgnored = isIgnored() ?: return null
return if (isIgnored) {

View File

@ -45,7 +45,7 @@ import im.vector.riotx.core.utils.startSharePlainTextIntent
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
import im.vector.riotx.features.roommemberprofile.powerlevel.SetPowerLevelDialogs
import im.vector.riotx.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.*
@ -85,7 +85,7 @@ class RoomMemberProfileFragment @Inject constructor(
}
}
memberProfileStateView.contentView = memberProfileInfoContainer
matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true)
matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true, disableItemAnimation = true)
roomMemberProfileController.callback = this
appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView,
listOf(
@ -112,7 +112,7 @@ class RoomMemberProfileFragment @Inject constructor(
}
private fun handleShowPowerLevelAdminWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelValidation) {
SetPowerLevelDialogs.showValidation(requireActivity()) {
EditPowerLevelDialogs.showValidation(requireActivity()) {
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false))
}
}
@ -253,9 +253,9 @@ class RoomMemberProfileFragment @Inject constructor(
navigator.openBigImageViewer(requireActivity(), view, userMatrixItem)
}
override fun onSetPowerLevel(userRole: Role) {
SetPowerLevelDialogs.showChoice(requireActivity(), userRole) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(userRole.value, newPowerLevel, true))
override fun onEditPowerLevel(currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), currentRole) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true))
}
}

View File

@ -33,7 +33,6 @@ 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.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.profile.ProfileService
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
@ -45,12 +44,12 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.mapOptional
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import kotlinx.coroutines.Dispatchers
@ -155,7 +154,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
if (room == null || action.previousValue == action.newValue) {
return@withState
}
val currentPowerLevelsContent = state.powerLevelsContent() ?: return@withState
val currentPowerLevelsContent = state.powerLevelsContent ?: return@withState
val myPowerLevel = PowerLevelsHelper(currentPowerLevelsContent).getUserPowerLevelValue(session.myUserId)
if (action.askForValidation && action.newValue >= myPowerLevel) {
_viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelValidation(action.previousValue, action.newValue))
@ -280,17 +279,22 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
private fun observeRoomSummaryAndPowerLevels(room: Room) {
val roomSummaryLive = room.rx().liveRoomSummary().unwrap()
val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
powerLevelsContentLive.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
val permissions = ActionPermissions(
canKick = powerLevelsHelper.canKick(session.myUserId),
canBan = powerLevelsHelper.canBan(session.myUserId),
canInvite = powerLevelsHelper.canInvite(session.myUserId),
canEditPowerLevel = powerLevelsHelper.isAllowedToSend(true, EventType.STATE_ROOM_POWER_LEVELS, session.myUserId)
)
setState { copy(powerLevelsContent = it, actionPermissions = permissions) }
}.disposeOnClear()
roomSummaryLive.execute {
copy(isRoomEncrypted = it.invoke()?.isEncrypted == true)
}
powerLevelsContentLive.execute {
copy(powerLevelsContent = it)
}
Observable
.combineLatest(
roomSummaryLive,

View File

@ -24,7 +24,6 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningIn
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.util.MatrixItem
import java.lang.reflect.Member
data class RoomMemberProfileViewState(
val userId: String,
@ -33,14 +32,22 @@ data class RoomMemberProfileViewState(
val isMine: Boolean = false,
val isIgnored: Async<Boolean> = Uninitialized,
val isRoomEncrypted: Boolean = false,
val powerLevelsContent: Async<PowerLevelsContent> = Uninitialized,
val powerLevelsContent: PowerLevelsContent? = null,
val userPowerLevelString: Async<String> = Uninitialized,
val userMatrixItem: Async<MatrixItem> = Uninitialized,
val userMXCrossSigningInfo: MXCrossSigningInfo? = null,
val allDevicesAreTrusted: Boolean = false,
val allDevicesAreCrossSignedTrusted: Boolean = false,
val asyncMembership: Async<Membership> = Uninitialized
val asyncMembership: Async<Membership> = Uninitialized,
val actionPermissions: ActionPermissions = ActionPermissions()
) : MvRxState {
constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId)
}
data class ActionPermissions(
val canKick: Boolean = false,
val canBan: Boolean = false,
val canInvite: Boolean = false,
val canEditPowerLevel: Boolean = false
)

View File

@ -25,18 +25,22 @@ import androidx.core.view.isVisible
import im.vector.matrix.android.api.session.room.powerlevels.Role
import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import kotlinx.android.synthetic.main.dialog_set_power_level.view.*
import kotlinx.android.synthetic.main.dialog_edit_power_level.view.*
object SetPowerLevelDialogs {
object EditPowerLevelDialogs {
private const val SLIDER_STEP = 1
private const val SLIDER_MAX_VALUE = 100
private const val SLIDER_MIN_VALUE = -100
fun showChoice(activity: Activity, currentRole: Role, listener: (Int) -> Unit) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_set_power_level, null)
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null)
dialogLayout.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId ->
dialogLayout.powerLevelCustomLayout.isVisible = checkedId == R.id.powerLevelCustomRadio
}
dialogLayout.powerLevelCustomSlider.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
dialogLayout.powerLevelCustomTitle.text = activity.getString(R.string.power_level_custom, progress)
dialogLayout.powerLevelCustomTitle.text = activity.getString(R.string.power_level_custom, seekBar.normalizedProgress())
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
@ -47,7 +51,8 @@ object SetPowerLevelDialogs {
//NOOP
}
})
dialogLayout.powerLevelCustomSlider.progress = currentRole.value
dialogLayout.powerLevelCustomSlider.max = (SLIDER_MAX_VALUE - SLIDER_MIN_VALUE) / SLIDER_STEP
dialogLayout.powerLevelCustomSlider.progress = SLIDER_MAX_VALUE + (currentRole.value * SLIDER_STEP)
when (currentRole) {
Role.Admin -> dialogLayout.powerLevelAdminRadio.isChecked = true
Role.Moderator -> dialogLayout.powerLevelModeratorRadio.isChecked = true
@ -56,15 +61,15 @@ object SetPowerLevelDialogs {
}
AlertDialog.Builder(activity)
.setTitle("Change power level")
.setTitle(R.string.power_level_edit_title)
.setView(dialogLayout)
.setPositiveButton(R.string.action_change)
.setPositiveButton(R.string.edit)
{ _, _ ->
val newValue = when (dialogLayout.powerLevelRadioGroup.checkedRadioButtonId) {
R.id.powerLevelAdminRadio -> Role.Admin.value
R.id.powerLevelModeratorRadio -> Role.Moderator.value
R.id.powerLevelDefaultRadio -> Role.Default.value
else -> dialogLayout.powerLevelCustomSlider.progress
else -> dialogLayout.powerLevelCustomSlider.normalizedProgress()
}
listener(newValue)
}
@ -84,6 +89,8 @@ object SetPowerLevelDialogs {
.show()
}
private fun SeekBar.normalizedProgress() = SLIDER_MIN_VALUE + (progress * SLIDER_STEP)
fun showValidation(activity: Activity, onValidate: () -> Unit) {
// ask to the user to confirmation thu upgrade.
AlertDialog.Builder(activity)

View File

@ -140,7 +140,7 @@ class RoomProfileFragment @Inject constructor(
private fun setupRecyclerView() {
roomProfileController.callback = this
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true)
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true)
}
override fun onDestroyView() {

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.roomprofile.members
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import com.airbnb.mvrx.args
@ -46,6 +47,13 @@ class RoomMemberListFragment @Inject constructor(
override fun getMenuRes() = R.menu.menu_room_member_list
override fun onPrepareOptionsMenu(menu: Menu) {
val canInvite = withState(viewModel) {
it.actionsPermissions.canInvite
}
menu.findItem(R.id.menu_room_member_list_add_member).isVisible = canInvite
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_room_member_list_add_member -> {
@ -61,6 +69,9 @@ class RoomMemberListFragment @Inject constructor(
roomMemberListController.callback = this
setupToolbar(roomSettingsToolbar)
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) {
invalidateOptionsMenu()
}
}
override fun onDestroyView() {

View File

@ -39,6 +39,7 @@ import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
@ -68,6 +69,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
init {
observeRoomMemberSummaries()
observeRoomSummary()
observePowerLevel()
}
private fun observeRoomMemberSummaries() {
@ -118,6 +120,18 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
}
}
private fun observePowerLevel() {
PowerLevelsObservableFactory(room).createObservable()
.subscribe {
val permissions = ActionPermissions(
canInvite = PowerLevelsHelper(it).canInvite(session.myUserId)
)
setState {
copy(actionsPermissions = permissions)
}
}.disposeOnClear()
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.unwrap()

View File

@ -30,12 +30,17 @@ data class RoomMemberListViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized,
val actionsPermissions: ActionPermissions = ActionPermissions()
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}
data class ActionPermissions(
val canInvite: Boolean = false
)
typealias RoomMemberSummaries = List<Pair<RoomMemberListCategories, List<RoomMemberSummary>>>
enum class RoomMemberListCategories(@StringRes val titleRes: Int) {

View File

@ -138,8 +138,6 @@
android:id="@+id/notificationAreaView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View File

@ -4,14 +4,16 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:background="@color/vector_fuchsia_color"
android:minHeight="48dp"
tools:parentTag="android.widget.RelativeLayout">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?vctr_list_divider_color" />
<ImageView
android:id="@+id/room_notification_icon"
android:id="@+id/roomNotificationIcon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
@ -20,14 +22,15 @@
tools:src="@drawable/vector_typing" />
<TextView
android:id="@+id/room_notification_message"
android:id="@+id/roomNotificationMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="64dp"
android:layout_marginEnd="16dp"
android:accessibilityLiveRegion="polite"
android:gravity="center"
android:textColor="?attr/vctr_room_notification_text_color"
tools:text="a text here" />
tools:text="@string/room_do_not_have_permission_to_post" />
</merge>

View File

@ -2433,5 +2433,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="identity_server_set_alternative_notice">Alternatively, you can enter any other identity server URL</string>
<string name="identity_server_set_alternative_notice_no_default">Enter the URL of an identity server</string>
<string name="identity_server_set_alternative_submit">Submit</string>
<string name="power_level_edit_title">Edit power level</string>
</resources>