mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Power levels: handle some action permissions
This commit is contained in:
parent
a1fd35aa67
commit
e5da5a34cb
@ -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.
|
||||
*
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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 && (
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user