Merge pull request #1459 from vector-im/feature/power_level

Feature/power level
This commit is contained in:
ganfra 2020-06-12 16:12:37 +02:00 committed by GitHub
commit 54b3af2c88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 1661 additions and 632 deletions

View file

@ -9,6 +9,7 @@ Improvements 🙌:
- New wording for notice when current user is the sender
- Hide "X made no changes" event by default in timeline (#1430)
- Hide left rooms in breadcrumbs (#766)
- Handle PowerLevel properly (#627)
- Correctly handle SSO login redirection
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
- Improve checking of homeserver version support (#1442)

View file

@ -39,6 +39,6 @@ class SenderNotificationPermissionCondition(
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
return event.senderId != null && powerLevelsHelper.getUserPowerLevel(event.senderId) >= powerLevelsHelper.notificationLevel(key)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key)
}
}

View file

@ -63,6 +63,27 @@ interface MembershipService {
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Ban a user from the room
*/
fun ban(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Unban a user from the room
*/
fun unban(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Kick a user from the room
*/
fun kick(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/**
* Join the room, or accept an invitation.
*/

View file

@ -44,6 +44,10 @@ enum class Membership(val value: String) {
return this == KNOCK || this == LEAVE || this == BAN
}
fun isActive(): Boolean {
return activeMemberships().contains(this)
}
companion object {
fun activeMemberships(): List<Membership> {
return listOf(INVITE, JOIN)

View file

@ -18,21 +18,21 @@ package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants
import im.vector.matrix.android.api.session.room.powerlevels.Role
/**
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
*/
@JsonClass(generateAdapter = true)
data class PowerLevelsContent(
@Json(name = "ban") val ban: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "kick") val kick: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "invite") val invite: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "redact") val redact: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "events_default") val eventsDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL,
@Json(name = "ban") val ban: Int = Role.Moderator.value,
@Json(name = "kick") val kick: Int = Role.Moderator.value,
@Json(name = "invite") val invite: Int = Role.Moderator.value,
@Json(name = "redact") val redact: Int = Role.Moderator.value,
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
@Json(name = "events") val events: MutableMap<String, Int> = HashMap(),
@Json(name = "users_default") val usersDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL,
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
@Json(name = "users") val users: MutableMap<String, Int> = HashMap(),
@Json(name = "state_default") val stateDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL,
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
@Json(name = "notifications") val notifications: Map<String, Any> = HashMap()
)

View file

@ -30,22 +30,34 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return the power level
*/
fun getUserPowerLevel(userId: String): Int {
fun getUserPowerLevelValue(userId: String): Int {
return powerLevelsContent.users.getOrElse(userId) {
powerLevelsContent.usersDefault
}
}
/**
* Returns the user power level of a dedicated user Id
*
* @param userId the user id
* @return the power level
*/
fun getUserRole(userId: String): Role {
val value = getUserPowerLevelValue(userId)
return Role.fromValue(value, powerLevelsContent.eventsDefault)
}
/**
* Tell if an user can send an event of a certain type
*
* @param userId the id of the user to check for.
* @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
*/
fun isAllowedToSend(isState: Boolean, eventType: String?, userId: String): Boolean {
fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevel(userId)
val powerLevel = getUserPowerLevelValue(userId)
val minimumPowerLevel = powerLevelsContent.events[eventType]
?: if (isState) {
powerLevelsContent.stateDefault
@ -56,6 +68,46 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
} else false
}
/**
* Check if the user have the necessary power level to invite
* @param userId the id of the user to check for.
* @return true if able to invite
*/
fun isUserAbleToInvite(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.invite
}
/**
* Check if the user have the necessary power level to ban
* @param userId the id of the user to check for.
* @return true if able to ban
*/
fun isUserAbleToBan(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.ban
}
/**
* Check if the user have the necessary power level to kick
* @param userId the id of the user to check for.
* @return true if able to kick
*/
fun isUserAbleToKick(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.kick
}
/**
* Check if the user have the necessary power level to redact
* @param userId the id of the user to check for.
* @return true if able to redact
*/
fun isUserAbleToRedact(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redact
}
/**
* Get the notification level for a dedicated key.
*
@ -67,7 +119,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
// the first implementation was a string value
is String -> value.toInt()
is Int -> value
else -> PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL
else -> Role.Moderator.value
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package im.vector.matrix.android.api.session.room.powerlevels
import androidx.annotation.StringRes
import im.vector.matrix.android.R
sealed class Role(open val value: Int, @StringRes val res: Int) : Comparable<Role> {
object Admin : Role(100, R.string.power_level_admin)
object Moderator : Role(50, R.string.power_level_moderator)
object Default : Role(0, R.string.power_level_default)
data class Custom(override val value: Int) : Role(value, R.string.power_level_custom)
override fun compareTo(other: Role): Int {
return value.compareTo(other.value)
}
companion object {
// Order matters, default value should be checked after defined roles
fun fromValue(value: Int, default: Int): Role {
return when (value) {
Admin.value -> Admin
Moderator.value -> Moderator
Default.value,
default -> Default
else -> Custom(value)
}
}
}
}

View file

@ -47,6 +47,10 @@ data class Optional<T : Any> constructor(private val value: T?) {
fun <T : Any> from(value: T?): Optional<T> {
return Optional(value)
}
fun <T: Any> empty(): Optional<T> {
return Optional(null)
}
}
}

View file

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.alias.RoomAliasDescription
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.membership.admin.UserIdAndReason
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
@ -243,6 +244,33 @@ internal interface RoomAPI {
fun leave(@Path("roomId") roomId: String,
@Body params: Map<String, String?>): Call<Unit>
/**
* Ban a user from the given room.
*
* @param roomId the room id
* @param userIdAndReason the banned user object (userId and reason for ban)
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/ban")
fun ban(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call<Unit>
/**
* unban a user from the given room.
*
* @param roomId the room id
* @param userIdAndReason the unbanned user object (userId and reason for unban)
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/unban")
fun unban(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call<Unit>
/**
* Kick a user from the given room.
*
* @param roomId the room id
* @param userIdAndReason the kicked user object (userId and reason for kicking)
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick")
fun kick(@Path("roomId") roomId: String, @Body userIdAndReason: UserIdAndReason): Call<Unit>
/**
* Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room.
* This cannot be undone.

View file

@ -34,6 +34,8 @@ import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTas
import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.admin.DefaultMembershipAdminTask
import im.vector.matrix.android.internal.session.room.membership.admin.MembershipAdminTask
import im.vector.matrix.android.internal.session.room.membership.joining.DefaultInviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.DefaultJoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
@ -142,6 +144,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindLeaveRoomTask(task: DefaultLeaveRoomTask): LeaveRoomTask
@Binds
abstract fun bindMembershipAdminTask(task: DefaultMembershipAdminTask): MembershipAdminTask
@Binds
abstract fun bindLoadRoomMembersTask(task: DefaultLoadRoomMembersTask): LoadRoomMembersTask

View file

@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.membership.admin.MembershipAdminTask
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
@ -48,6 +49,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val inviteTask: InviteTask,
private val joinTask: JoinRoomTask,
private val leaveRoomTask: LeaveRoomTask,
private val membershipAdminTask: MembershipAdminTask,
@UserId
private val userId: String
) : MembershipService {
@ -113,6 +115,33 @@ internal class DefaultMembershipService @AssistedInject constructor(
}
}
override fun ban(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.BAN, roomId, userId, reason)
return membershipAdminTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun unban(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.UNBAN, roomId, userId, reason)
return membershipAdminTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun kick(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = MembershipAdminTask.Params(MembershipAdminTask.Type.KICK, roomId, userId, reason)
return membershipAdminTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteTask.Params(roomId, userId, reason)
return inviteTask

View file

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.membership
import im.vector.matrix.android.api.session.events.model.Event
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.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import io.realm.Realm
@ -35,7 +34,7 @@ internal class RoomMemberEventHandler @Inject constructor() {
val userId = event.stateKey ?: return false
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership in Membership.activeMemberships()) {
if (roomMember.membership.isActive()) {
val userEntity = UserEntityFactory.create(userId, roomMember)
realm.insertOrUpdate(userEntity)
}

View file

@ -0,0 +1,52 @@
/*
* 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.matrix.android.internal.session.room.membership.admin
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface MembershipAdminTask : Task<MembershipAdminTask.Params, Unit> {
enum class Type {
BAN,
UNBAN,
KICK
}
data class Params(
val type: Type,
val roomId: String,
val userId: String,
val reason: String?
)
}
internal class DefaultMembershipAdminTask @Inject constructor(private val roomAPI: RoomAPI) : MembershipAdminTask {
override suspend fun execute(params: MembershipAdminTask.Params) {
val userIdAndReason = UserIdAndReason(params.userId, params.reason)
executeRequest<Unit>(null) {
apiCall = when (params.type) {
MembershipAdminTask.Type.BAN -> roomAPI.ban(params.roomId, userIdAndReason)
MembershipAdminTask.Type.UNBAN -> roomAPI.unban(params.roomId, userIdAndReason)
MembershipAdminTask.Type.KICK -> roomAPI.kick(params.roomId, userIdAndReason)
}
}
}
}

View file

@ -0,0 +1,25 @@
/*
* 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.matrix.android.internal.session.room.membership.admin
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserIdAndReason(
@Json(name = "user_id") val userId: String,
@Json(name = "reason") val reason: String? = null
)

View file

@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom
@ -42,6 +43,7 @@ import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
@ -67,6 +69,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler,
@UserId private val userId: String,
private val eventBus: EventBus) {
sealed class HandlingStrategy {
@ -208,9 +211,37 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
roomId: String,
roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
roomEntity.membership = Membership.LEAVE
for (event in roomSync.state?.events.orEmpty()) {
if (event.eventId == null || event.stateKey == null) {
continue
}
val eventEntity = event.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm)
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = event.eventId
root = eventEntity
}
roomMemberEventHandler.handle(realm, roomId, event)
}
for (event in roomSync.timeline?.events.orEmpty()) {
if (event.eventId == null || event.senderId == null) {
continue
}
val eventEntity = event.toEntity(roomId, SendState.SYNCED).copyToRealmOrIgnore(realm)
if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = event.eventId
root = eventEntity
}
if (event.type == EventType.STATE_ROOM_MEMBER) {
roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
}
}
}
val leftMember = RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()
val membership = leftMember?.membership ?: Membership.LEAVE
roomEntity.membership = membership
roomEntity.chunks.deleteAllFromRealm()
roomSummaryUpdater.update(realm, roomId, Membership.LEAVE, roomSync.summary, roomSync.unreadNotifications)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications)
return roomEntity
}

View file

@ -198,6 +198,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
stateKey = QueryStringValue.NoCondition
)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isAllowedToSend(true, null, userId)
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null)
}
}

View file

@ -89,6 +89,19 @@
<string name="notice_widget_modified">%1$s modified %2$s widget</string>
<string name="notice_widget_modified_by_you">You modified %1$s widget</string>
<string name="power_level_admin">Admin</string>
<string name="power_level_moderator">Moderator</string>
<string name="power_level_default">Default</string>
<string name="power_level_custom">Custom (%1$d)</string>
<string name="power_level_custom_no_value">Custom</string>
<!-- parameter will be a comma separated list of values of notice_power_level_diff -->
<string name="notice_power_level_changed_by_you">You changed the power level of %1$s.</string>
<!-- First parameter will be a userId or display name, second one will be a comma separated list of values of notice_power_level_diff -->
<string name="notice_power_level_changed">%1$s changed the power level of %2$s.</string>
<!-- First parameter will be a userId or display name, the two last ones will be value of power_level_* -->
<string name="notice_power_level_diff">%1$s from %2$s to %3$s</string>
<string name="notice_crypto_unable_to_decrypt">** Unable to decrypt: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">The sender\'s device has not sent us the keys for this message.</string>

View file

@ -248,7 +248,7 @@ android {
dependencies {
def epoxy_version = '3.9.0'
def epoxy_version = '3.11.0'
def fragment_version = '1.2.0'
def arrow_version = "0.8.2"
def coroutines_version = "1.3.2"

View file

@ -32,6 +32,7 @@ import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.home.HomeModule
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
@ -57,7 +58,9 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileActivity
import im.vector.riotx.features.roommemberprofile.devices.DeviceListBottomSheet
import im.vector.riotx.features.roomprofile.RoomProfileActivity
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet
import im.vector.riotx.features.share.IncomingShareActivity
@ -101,6 +104,9 @@ interface ScreenComponent {
* ========================================================================================== */
fun inject(activity: HomeActivity)
fun inject(activity: RoomDetailActivity)
fun inject(activity: RoomProfileActivity)
fun inject(activity: RoomMemberProfileActivity)
fun inject(activity: VectorSettingsActivity)
fun inject(activity: KeysBackupManageActivity)
fun inject(activity: EmojiReactionPickerActivity)

View file

@ -0,0 +1,61 @@
/*
* 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.core.dialogs
import android.app.Activity
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import im.vector.riotx.R
import kotlinx.android.synthetic.main.dialog_confirmation_with_reason.view.*
object ConfirmationDialogBuilder {
fun show(activity: Activity,
askForReason: Boolean,
@StringRes titleRes: Int,
@StringRes confirmationRes: Int,
@StringRes positiveRes: Int,
@StringRes reasonHintRes: Int,
confirmation: (String?) -> Unit) {
val layout = activity.layoutInflater.inflate(R.layout.dialog_confirmation_with_reason, null)
layout.dialogConfirmationText.setText(confirmationRes)
layout.dialogReasonCheck.isVisible = askForReason
layout.dialogReasonTextInputLayout.isVisible = askForReason
layout.dialogReasonCheck.setOnCheckedChangeListener { _, isChecked ->
layout.dialogReasonTextInputLayout.isEnabled = isChecked
}
if (askForReason && reasonHintRes != 0) {
layout.dialogReasonInput.setHint(reasonHintRes)
}
AlertDialog.Builder(activity)
.setTitle(titleRes)
.setView(layout)
.setPositiveButton(positiveRes) { _, _ ->
val reason = layout.dialogReasonInput.text.toString()
.takeIf { askForReason }
?.takeIf { layout.dialogReasonCheck.isChecked }
?.takeIf { it.isNotBlank() }
confirmation(reason)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}

View file

@ -66,7 +66,6 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
if (listener == null) {
holder.view.isClickable = false
}
holder.editable.isVisible = editable
holder.title.text = title
val tintColor = if (destructive) {
ContextCompat.getColor(holder.view.context, R.color.riotx_notice)
@ -94,7 +93,7 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
holder.secondaryAccessory.isVisible = false
}
if (editableRes != 0) {
if (editableRes != 0 && editable) {
val tintColorSecondary = if (destructive) {
tintColor
} else {

View file

@ -18,23 +18,19 @@ package im.vector.riotx.core.ui.views
import android.content.Context
import android.graphics.Color
import android.text.SpannableString
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import butterknife.BindView
import butterknife.ButterKnife
import androidx.core.text.italic
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 +45,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,13 +68,9 @@ 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()
is State.Typing -> renderTyping(newState)
is State.UnreadPreview -> renderUnreadPreview()
is State.ScrollToBottom -> renderScrollToBottom(newState)
is State.UnsentEvents -> renderUnsent(newState)
}
}
@ -91,30 +78,27 @@ 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 renderTombstone(state: State.Tombstone) {
private fun renderNoPermissionToPost() {
visibility = View.VISIBLE
imageView.setImageResource(R.drawable.error)
roomNotificationIcon.setImageDrawable(null)
val message = span {
+resources.getString(R.string.room_tombstone_versioned_description)
+"\n"
span(resources.getString(R.string.room_tombstone_continuation_link)) {
textDecorationLine = "underline"
onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) }
italic {
+resources.getString(R.string.room_do_not_have_permission_to_post)
}
}
messageView.movementMethod = BetterLinkMovementMethod.getInstance()
messageView.text = message
roomNotificationMessage.text = message
roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.riotx_text_secondary))
}
private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) {
@ -130,73 +114,26 @@ 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() {
private fun renderTombstone(state: State.Tombstone) {
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))
}
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))
}
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() }
}
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))
} else {
imageView.setImageResource(R.drawable.scrolldown)
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
if (!state.message.isNullOrEmpty()) {
messageView.text = SpannableString(state.message)
roomNotificationIcon.setImageResource(R.drawable.error)
val message = span {
+resources.getString(R.string.room_tombstone_versioned_description)
+"\n"
span(resources.getString(R.string.room_tombstone_continuation_link)) {
textDecorationLine = "underline"
onClick = { delegate?.onTombstoneEventClicked(state.tombstoneEvent) }
}
}
messageView.setOnClickListener { delegate?.jumpToBottom() }
imageView.setOnClickListener { delegate?.jumpToBottom() }
}
private fun renderUnsent(state: State.UnsentEvents) {
visibility = View.VISIBLE
imageView.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
val message = context.getString(messageRes, resendAll, cancelAll)
val cancelAllPos = message.indexOf(cancelAll)
val resendAllPos = message.indexOf(resendAll)
val spannableString = SpannableString(message)
// cancelAllPos should always be > 0 but a GA crash reported here
if (cancelAllPos >= 0) {
spannableString.setSpan(CancelAllClickableSpan(), cancelAllPos, cancelAllPos + cancelAll.length, 0)
}
// resendAllPos should always be > 0 but a GA crash reported here
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 = BetterLinkMovementMethod.getInstance()
roomNotificationMessage.text = message
}
private fun renderDefault() {
@ -207,44 +144,9 @@ class NotificationAreaView @JvmOverloads constructor(
visibility = View.GONE
}
/**
* Track the cancel all click.
*/
private inner class CancelAllClickableSpan : ClickableSpan() {
override fun onClick(widget: View) {
delegate?.deleteUnsentEvents()
render(state)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color)
ds.bgColor = 0
ds.isUnderlineText = true
}
}
/**
* Track the resend all click.
*/
private inner class ResendAllClickableSpan : ClickableSpan() {
override fun onClick(widget: View) {
delegate?.resendUnsentEvents()
render(state)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color)
ds.bgColor = 0
ds.isUnderlineText = true
}
}
/**
* The state representing the view
* It can take one state at a time
* Priority of state is managed in {@link VectorRoomActivity.refreshNotificationsArea() }
*/
sealed class State {
@ -254,29 +156,17 @@ class NotificationAreaView @JvmOverloads constructor(
// View will be Invisible
object Default : State()
// User can't post messages to room because his power level doesn't allow it.
object NoPermissionToPost : State()
// View will be Gone
object Hidden : State()
// Resource limit exceeded error will be displayed (only hard for the moment)
data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State()
// Server connection is lost
object ConnectionError : State()
// The room is dead
data class Tombstone(val tombstoneEvent: Event) : State()
// Somebody is typing
data class Typing(val message: String) : State()
// Some new messages are unread in preview
object UnreadPreview : State()
// Some new messages are unread (grey or red)
data class ScrollToBottom(val unreadCount: Int, val message: String? = null) : State()
// Some event has been unsent
data class UnsentEvents(val hasUndeliverableEvents: Boolean, val hasUnknownDeviceEvents: Boolean) : State()
// Resource limit exceeded error will be displayed (only hard for the moment)
data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State()
}
/**
@ -284,31 +174,5 @@ class NotificationAreaView @JvmOverloads constructor(
*/
interface Delegate {
fun onTombstoneEventClicked(tombstoneEvent: Event)
fun resendUnsentEvents()
fun deleteUnsentEvents()
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

@ -19,23 +19,48 @@ package im.vector.riotx.features.home.room.detail
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.riotx.features.room.RequireActiveMembershipAction
import im.vector.riotx.features.room.RequireActiveMembershipViewEvents
import im.vector.riotx.features.room.RequireActiveMembershipViewModel
import im.vector.riotx.features.room.RequireActiveMembershipViewState
import kotlinx.android.synthetic.main.activity_room_detail.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
class RoomDetailActivity :
VectorBaseActivity(),
ToolbarConfigurable,
RequireActiveMembershipViewModel.Factory {
override fun getLayoutRes() = R.layout.activity_room_detail
private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel()
@Inject
lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory
override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel {
// Due to shortcut, we cannot use MvRx args. Pass the first roomId here
return requireActiveMembershipViewModelFactory.create(initialState.copy(roomId = currentRoomId ?: ""))
}
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
// Simple filter
private var currentRoomId: String? = null
@ -68,14 +93,27 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
}
.disposeOnDestroy()
requireActiveMembershipViewModel.observeViewEvents {
when (it) {
is RequireActiveMembershipViewEvents.RoomLeft -> handleRoomLeft(it)
}
}
drawerLayout.addDrawerListener(drawerListener)
}
private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) {
if (roomLeft.leftMessage != null) {
Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show()
}
finish()
}
private fun switchToRoom(switchToRoom: RoomDetailSharedAction.SwitchToRoom) {
drawerLayout.closeDrawer(GravityCompat.START)
// Do not replace the Fragment if it's the same roomId
if (currentRoomId != switchToRoom.roomId) {
currentRoomId = switchToRoom.roomId
requireActiveMembershipViewModel.handle(RequireActiveMembershipAction.ChangeRoom(switchToRoom.roomId))
replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(switchToRoom.roomId))
}
}
@ -94,7 +132,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
hideKeyboard()
if (!drawerLayout.isDrawerOpen(GravityCompat.START) && newState == DrawerLayout.STATE_DRAGGING) {
// User is starting to open the drawer, scroll the list to op
// User is starting to open the drawer, scroll the list to top
scrollBreadcrumbsToTop()
}
}
@ -125,6 +163,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
// Shortcuts can't have intents with parcelables
fun shortcutIntent(context: Context, roomId: String): Intent {
return Intent(context, RoomDetailActivity::class.java).apply {
action = ACTION_ROOM_DETAILS_FROM_SHORTCUT

View file

@ -56,10 +56,8 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.permalinks.PermalinkFactory
@ -88,6 +86,7 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.extensions.cleanup
@ -285,7 +284,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 +374,7 @@ class RoomDetailFragment @Inject constructor(
modelBuildListener = null
debouncer.cancelAll()
recyclerView.cleanup()
super.onDestroyView()
}
@ -442,22 +445,6 @@ class RoomDetailFragment @Inject constructor(
override fun onTombstoneEventClicked(tombstoneEvent: Event) {
roomDetailViewModel.handle(RoomDetailAction.HandleTombstoneEvent(tombstoneEvent))
}
override fun resendUnsentEvents() {
vectorBaseActivity.notImplemented()
}
override fun deleteUnsentEvents() {
vectorBaseActivity.notImplemented()
}
override fun closeScreen() {
vectorBaseActivity.notImplemented()
}
override fun jumpToBottom() {
vectorBaseActivity.notImplemented()
}
}
}
@ -611,6 +598,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,
@ -735,37 +728,35 @@ 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) {
state.asyncRoomSummary()?.let { roomSummary ->
if (roomSummary.membership.isLeft()) {
Timber.w("The room has been left")
activity?.finish()
} else {
roomToolbarTitleView.text = roomSummary.displayName
avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView)
roomToolbarTitleView.text = roomSummary.displayName
avatarRenderer.render(roomSummary.toMatrixItem(), roomToolbarAvatarImageView)
renderSubTitle(state.typingMessage, roomSummary.topic)
}
renderSubTitle(state.typingMessage, roomSummary.topic)
jumpToBottomView.count = roomSummary.notificationCount
jumpToBottomView.drawBadge = roomSummary.hasUnreadMessages
@ -865,28 +856,17 @@ class RoomDetailFragment @Inject constructor(
}
private fun promptConfirmationToRedactEvent(action: EventSharedAction.Redact) {
val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_delete_event, null)
val reasonCheckBox = layout.findViewById<MaterialCheckBox>(R.id.deleteEventReasonCheck)
val reasonTextInputLayout = layout.findViewById<TextInputLayout>(R.id.deleteEventReasonTextInputLayout)
val reasonInput = layout.findViewById<TextInputEditText>(R.id.deleteEventReasonInput)
reasonCheckBox.isVisible = action.askForReason
reasonTextInputLayout.isVisible = action.askForReason
reasonCheckBox.setOnCheckedChangeListener { _, isChecked -> reasonTextInputLayout.isEnabled = isChecked }
AlertDialog.Builder(requireActivity())
.setTitle(R.string.delete_event_dialog_title)
.setView(layout)
.setPositiveButton(R.string.remove) { _, _ ->
val reason = reasonInput.text.toString()
.takeIf { action.askForReason }
?.takeIf { reasonCheckBox.isChecked }
?.takeIf { it.isNotBlank() }
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = action.askForReason,
confirmationRes = R.string.delete_event_dialog_content,
positiveRes = R.string.remove,
reasonHintRes = R.string.delete_event_dialog_reason_hint,
titleRes = R.string.delete_event_dialog_title
) { reason ->
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
@ -1380,7 +1360,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
@ -72,6 +73,7 @@ import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerat
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.home.room.typing.TypingHelper
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
@ -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,17 @@ class RoomDetailViewModel @AssistedInject constructor(
session.onRoomDisplayed(initialState.roomId)
}
private fun observePowerLevel() {
PowerLevelsObservableFactory(room).createObservable()
.subscribe {
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
setState {
copy(canSendMessage = canSendMessage)
}
}
.disposeOnClear()
}
private fun observeActiveRoomWidgets() {
session.rx()
.liveRoomWidgets(
@ -408,16 +422,16 @@ class RoomDetailViewModel @AssistedInject constructor(
popDraft()
}
is ParsedCommand.UnbanUser -> {
// TODO
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
handleUnbanSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.BanUser -> {
// TODO
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
handleBanSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.KickUser -> {
// TODO
_viewEvents.post(RoomDetailViewEvents.SlashCommandNotImplemented)
handleKickSlashCommand(slashCommandResult)
popDraft()
}
is ParsedCommand.JoinRoom -> {
handleJoinToAnotherRoomSlashCommand(slashCommandResult)
@ -603,23 +617,38 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
room.updateTopic(changeTopic.topic, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
})
launchSlashCommandFlow {
room.updateTopic(changeTopic.topic, it)
}
}
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
launchSlashCommandFlow {
room.invite(invite.userId, invite.reason, it)
}
}
room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> {
private fun handleKickSlashCommand(kick: ParsedCommand.KickUser) {
launchSlashCommandFlow {
room.kick(kick.userId, kick.reason, it)
}
}
private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) {
launchSlashCommandFlow {
room.ban(ban.userId, ban.reason, it)
}
}
private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) {
launchSlashCommandFlow {
room.unban(unban.userId, unban.reason, it)
}
}
private fun launchSlashCommandFlow(lambda: (MatrixCallback<Unit>) -> Unit) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
val matrixCallback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultOk)
}
@ -627,7 +656,8 @@ class RoomDetailViewModel @AssistedInject constructor(
override fun onFailure(failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.SlashCommandResultError(failure))
}
})
}
lambda.invoke(matrixCallback)
}
private fun handleSendReaction(action: RoomDetailAction.SendReaction) {

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 = true
) : 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.isUserAllowedToSend(session.myUserId, false, EventType.REACTION)
val canRedact = powerLevelsHelper.isUserAbleToRedact(session.myUserId)
val canSendMessage = powerLevelsHelper.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
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

@ -59,6 +59,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.REACTION,
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_ENCRYPTION -> {

View file

@ -22,6 +22,7 @@ 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.model.GuestAccess
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.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomGuestAccessContent
@ -34,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.widgets.model.WidgetContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@ -64,6 +66,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.STATE_ROOM_WIDGET,
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
@ -84,6 +87,34 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
}
}
private fun formatRoomPowerLevels(event: Event, disambiguatedDisplayName: String): CharSequence? {
val powerLevelsContent: PowerLevelsContent = event.getClearContent().toModel() ?: return null
val previousPowerLevelsContent: PowerLevelsContent = event.prevContent.toModel() ?: return null
val userIds = HashSet<String>()
userIds.addAll(powerLevelsContent.users.keys)
userIds.addAll(previousPowerLevelsContent.users.keys)
val diffs = ArrayList<String>()
userIds.forEach { userId ->
val from = PowerLevelsHelper(previousPowerLevelsContent).getUserRole(userId)
val to = PowerLevelsHelper(powerLevelsContent).getUserRole(userId)
if (from != to) {
val fromStr = sp.getString(from.res, from.value)
val toStr = sp.getString(to.res, to.value)
val diff = sp.getString(R.string.notice_power_level_diff, userId, fromStr, toStr)
diffs.add(diff)
}
}
if (diffs.isEmpty()) {
return null
}
val diffStr = diffs.joinToString(separator = ", ")
return if (event.isSentByCurrentUser()) {
sp.getString(R.string.notice_power_level_changed_by_you, diffStr)
} else {
sp.getString(R.string.notice_power_level_changed, disambiguatedDisplayName, diffStr)
}
}
private fun formatWidgetEvent(event: Event, disambiguatedDisplayName: String): CharSequence? {
val widgetContent: WidgetContent = event.getClearContent().toModel() ?: return null
val previousWidgetContent: WidgetContent? = event.prevContent.toModel()

View file

@ -32,6 +32,7 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_ALIASES,
EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_POWER_LEVELS,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,

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

@ -1,25 +1,23 @@
/*
* Copyright 2020 New Vector Ltd
* 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
* 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.matrix.android.api.session.room.powerlevels
package im.vector.riotx.features.room
object PowerLevelsConstants {
import im.vector.riotx.core.platform.VectorViewModelAction
const val DEFAULT_ROOM_ADMIN_LEVEL = 100
const val DEFAULT_ROOM_MODERATOR_LEVEL = 50
const val DEFAULT_ROOM_USER_LEVEL = 0
sealed class RequireActiveMembershipAction : VectorViewModelAction {
data class ChangeRoom(val roomId: String) : RequireActiveMembershipAction()
}

View file

@ -0,0 +1,23 @@
/*
* 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.room
import im.vector.riotx.core.platform.VectorViewEvents
sealed class RequireActiveMembershipViewEvents : VectorViewEvents {
data class RoomLeft(val leftMessage: String?) : RequireActiveMembershipViewEvents()
}

View file

@ -0,0 +1,134 @@
/*
* 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.room
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
/**
* This ViewModel observe a room summary and notify when the room is left
*/
class RequireActiveMembershipViewModel @AssistedInject constructor(
@Assisted initialState: RequireActiveMembershipViewState,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<RequireActiveMembershipViewState, RequireActiveMembershipAction, RequireActiveMembershipViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel
}
companion object : MvRxViewModelFactory<RequireActiveMembershipViewModel, RequireActiveMembershipViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RequireActiveMembershipViewState): RequireActiveMembershipViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
private val roomIdObservable = BehaviorRelay.createDefault(Optional.from(initialState.roomId))
init {
observeRoomSummary()
}
private fun observeRoomSummary() {
roomIdObservable
.unwrap()
.switchMap { roomId ->
val room = session.getRoom(roomId) ?: return@switchMap Observable.just(Optional.empty<RequireActiveMembershipViewEvents.RoomLeft>())
room.rx()
.liveRoomSummary()
.unwrap()
.observeOn(Schedulers.computation())
.map { mapToLeftViewEvent(room, it) }
}
.unwrap()
.subscribe { event ->
_viewEvents.post(event)
}
.disposeOnClear()
}
private fun mapToLeftViewEvent(room: Room, roomSummary: RoomSummary): Optional<RequireActiveMembershipViewEvents.RoomLeft> {
if (roomSummary.membership.isActive()) {
return Optional.empty()
}
val senderId = room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId))?.senderId
val senderDisplayName = senderId?.takeIf { it != session.myUserId }?.let {
room.getRoomMember(it)?.displayName ?: it
}
val viewEvent = when (roomSummary.membership) {
Membership.LEAVE -> {
val message = senderDisplayName?.let {
stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it)
}
RequireActiveMembershipViewEvents.RoomLeft(message)
}
Membership.KNOCK -> {
val message = senderDisplayName?.let {
stringProvider.getString(R.string.has_been_kicked, roomSummary.displayName, it)
}
RequireActiveMembershipViewEvents.RoomLeft(message)
}
Membership.BAN -> {
val message = senderDisplayName?.let {
stringProvider.getString(R.string.has_been_banned, roomSummary.displayName, it)
}
RequireActiveMembershipViewEvents.RoomLeft(message)
}
else -> null
}
return Optional.from(viewEvent)
}
override fun handle(action: RequireActiveMembershipAction) {
when (action) {
is RequireActiveMembershipAction.ChangeRoom -> {
setState {
copy(roomId = action.roomId)
}
roomIdObservable.accept(Optional.from(action.roomId))
}
}.exhaustive
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.room
import com.airbnb.mvrx.MvRxState
import im.vector.riotx.features.roommemberprofile.RoomMemberProfileArgs
import im.vector.riotx.features.roomprofile.RoomProfileArgs
data class RequireActiveMembershipViewState(
val roomId: String? = null
) : MvRxState {
// No constructor for RoomDetailArgs because of intent for Shortcut
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId)
}

View file

@ -22,6 +22,10 @@ import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomMemberProfileAction : VectorViewModelAction {
object RetryFetchingInfo : RoomMemberProfileAction()
object IgnoreUser : RoomMemberProfileAction()
data class BanOrUnbanUser(val reason: String?) : RoomMemberProfileAction()
data class KickUser(val reason: String?) : RoomMemberProfileAction()
object InviteUser : RoomMemberProfileAction()
object VerifyUser : RoomMemberProfileAction()
object ShareRoomMemberProfile : RoomMemberProfileAction()
data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction()
}

View file

@ -19,36 +19,70 @@ package im.vector.riotx.features.roommemberprofile
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.room.RequireActiveMembershipViewEvents
import im.vector.riotx.features.room.RequireActiveMembershipViewModel
import im.vector.riotx.features.room.RequireActiveMembershipViewState
import javax.inject.Inject
class RoomMemberProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
class RoomMemberProfileActivity :
VectorBaseActivity(),
ToolbarConfigurable,
RequireActiveMembershipViewModel.Factory {
companion object {
private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS"
fun newIntent(context: Context, args: RoomMemberProfileArgs): Intent {
return Intent(context, RoomMemberProfileActivity::class.java).apply {
putExtra(EXTRA_FRAGMENT_ARGS, args)
putExtra(MvRx.KEY_ARG, args)
}
}
}
private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel()
@Inject
lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory
override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel {
return requireActiveMembershipViewModelFactory.create(initialState)
}
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun getLayoutRes() = R.layout.activity_simple
override fun initUiAndData() {
if (isFirstCreation()) {
val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS)
?: return
val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
addFragment(R.id.simpleFragmentContainer, RoomMemberProfileFragment::class.java, fragmentArgs)
}
requireActiveMembershipViewModel.observeViewEvents {
when (it) {
is RequireActiveMembershipViewEvents.RoomLeft -> handleRoomLeft(it)
}
}
}
override fun configure(toolbar: Toolbar) {
configureToolbar(toolbar)
}
private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) {
if (roomLeft.leftMessage != null) {
Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show()
}
finish()
}
}

View file

@ -18,6 +18,10 @@
package im.vector.riotx.features.roommemberprofile
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.room.powerlevels.Role
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.profiles.buildProfileAction
import im.vector.riotx.core.epoxy.profiles.buildProfileSection
@ -28,7 +32,8 @@ import javax.inject.Inject
class RoomMemberProfileController @Inject constructor(
private val stringProvider: StringProvider,
colorProvider: ColorProvider
colorProvider: ColorProvider,
private val session: Session
) : TypedEpoxyController<RoomMemberProfileViewState>() {
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
@ -42,6 +47,11 @@ class RoomMemberProfileController @Inject constructor(
fun onShowDeviceListNoCrossSigning()
fun onJumpToReadReceiptClicked()
fun onMentionClicked()
fun onEditPowerLevel(currentRole: Role)
fun onKickClicked()
fun onBanClicked(isUserBanned: Boolean)
fun onCancelInviteClicked()
fun onInviteClicked()
}
override fun buildModels(data: RoomMemberProfileViewState?) {
@ -71,6 +81,12 @@ class RoomMemberProfileController @Inject constructor(
}
private fun buildRoomMemberActions(state: RoomMemberProfileViewState) {
buildSecuritySection(state)
buildMoreSection(state)
buildAdminSection(state)
}
private fun buildSecuritySection(state: RoomMemberProfileViewState) {
// Security
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
@ -148,9 +164,13 @@ class RoomMemberProfileController @Inject constructor(
centered(false)
}
}
}
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",
@ -170,6 +190,19 @@ class RoomMemberProfileController @Inject constructor(
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",
@ -184,6 +217,77 @@ class RoomMemberProfileController @Inject constructor(
}
}
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.isMine && state.actionPermissions.canKick
val canBan = !state.isMine && 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) {
when (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() }
)
}
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() }
)
}
else -> Unit
}
}
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

@ -29,10 +29,12 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.room.powerlevels.Role
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.animations.AppBarStateChangeListener
import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener
import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
@ -43,6 +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.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.*
@ -82,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(
@ -94,15 +97,33 @@ class RoomMemberProfileFragment @Inject constructor(
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
viewModel.observeViewEvents {
when (it) {
is RoomMemberProfileViewEvents.Loading -> showLoading(it.message)
is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it)
is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink)
is RoomMemberProfileViewEvents.Loading -> showLoading(it.message)
is RoomMemberProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomMemberProfileViewEvents.StartVerification -> handleStartVerification(it)
is RoomMemberProfileViewEvents.ShareRoomMemberProfile -> handleShareRoomMemberProfile(it.permalink)
is RoomMemberProfileViewEvents.ShowPowerLevelValidation -> handleShowPowerLevelAdminWarning(it)
is RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning -> handleShowPowerLevelDemoteWarning(it)
is RoomMemberProfileViewEvents.OnKickActionSuccess -> Unit
is RoomMemberProfileViewEvents.OnSetPowerLevelSuccess -> Unit
is RoomMemberProfileViewEvents.OnBanActionSuccess -> Unit
is RoomMemberProfileViewEvents.OnIgnoreActionSuccess -> Unit
is RoomMemberProfileViewEvents.OnInviteActionSuccess -> Unit
}.exhaustive
}
}
private fun handleShowPowerLevelDemoteWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning) {
EditPowerLevelDialogs.showDemoteWarning(requireActivity()) {
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false))
}
}
private fun handleShowPowerLevelAdminWarning(event: RoomMemberProfileViewEvents.ShowPowerLevelValidation) {
EditPowerLevelDialogs.showValidation(requireActivity()) {
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(event.currentValue, event.newValue, false))
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.roomMemberProfileShareAction -> {
@ -207,8 +228,31 @@ class RoomMemberProfileFragment @Inject constructor(
// RoomMemberProfileController.Callback
override fun onIgnoreClicked() {
viewModel.handle(RoomMemberProfileAction.IgnoreUser)
override fun onIgnoreClicked() = withState(viewModel) { state ->
val isIgnored = state.isIgnored() ?: false
val titleRes: Int
val positiveButtonRes: Int
val confirmationRes: Int
if (isIgnored) {
confirmationRes = R.string.room_participants_action_unignore_prompt_msg
titleRes = R.string.room_participants_action_unignore_title
positiveButtonRes = R.string.room_participants_action_unignore
} else {
confirmationRes = R.string.room_participants_action_ignore_prompt_msg
titleRes = R.string.room_participants_action_ignore_title
positiveButtonRes = R.string.room_participants_action_ignore
}
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = false,
confirmationRes = confirmationRes,
positiveRes = positiveButtonRes,
reasonHintRes = 0,
titleRes = titleRes
) {
viewModel.handle(RoomMemberProfileAction.IgnoreUser)
}
}
override fun onTapVerify() {
@ -238,4 +282,68 @@ class RoomMemberProfileFragment @Inject constructor(
private fun onAvatarClicked(view: View, userMatrixItem: MatrixItem) {
navigator.openBigImageViewer(requireActivity(), view, userMatrixItem)
}
override fun onEditPowerLevel(currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), currentRole) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true))
}
}
override fun onKickClicked() {
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = true,
confirmationRes = R.string.room_participants_kick_prompt_msg,
positiveRes = R.string.room_participants_action_kick,
reasonHintRes = R.string.room_participants_kick_reason,
titleRes = R.string.room_participants_kick_title
) { reason ->
viewModel.handle(RoomMemberProfileAction.KickUser(reason))
}
}
override fun onBanClicked(isUserBanned: Boolean) {
val titleRes: Int
val positiveButtonRes: Int
val confirmationRes: Int
if (isUserBanned) {
confirmationRes = R.string.room_participants_unban_prompt_msg
titleRes = R.string.room_participants_unban_title
positiveButtonRes = R.string.room_participants_action_unban
} else {
confirmationRes = R.string.room_participants_ban_prompt_msg
titleRes = R.string.room_participants_ban_title
positiveButtonRes = R.string.room_participants_action_ban
}
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = !isUserBanned,
confirmationRes = confirmationRes,
positiveRes = positiveButtonRes,
reasonHintRes = R.string.room_participants_ban_reason,
titleRes = titleRes
) { reason ->
viewModel.handle(RoomMemberProfileAction.BanOrUnbanUser(reason))
}
}
override fun onCancelInviteClicked() {
ConfirmationDialogBuilder
.show(
activity = requireActivity(),
askForReason = false,
confirmationRes = R.string.room_participants_action_cancel_invite_prompt_msg,
positiveRes = R.string.room_participants_action_cancel_invite,
reasonHintRes = 0,
titleRes = R.string.room_participants_action_cancel_invite_title
) {
viewModel.handle(RoomMemberProfileAction.KickUser(null))
}
}
override fun onInviteClicked() {
viewModel.handle(RoomMemberProfileAction.InviteUser)
}
}

View file

@ -26,6 +26,12 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents()
object OnIgnoreActionSuccess : RoomMemberProfileViewEvents()
object OnSetPowerLevelSuccess : RoomMemberProfileViewEvents()
object OnInviteActionSuccess : RoomMemberProfileViewEvents()
object OnKickActionSuccess : RoomMemberProfileViewEvents()
object OnBanActionSuccess : RoomMemberProfileViewEvents()
data class ShowPowerLevelValidation(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents()
data class ShowPowerLevelDemoteWarning(val currentValue: Int, val newValue: Int) : RoomMemberProfileViewEvents()
data class StartVerification(
val userId: String,

View file

@ -18,7 +18,9 @@
package im.vector.riotx.features.roommemberprofile
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
@ -30,23 +32,25 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.events.model.toContent
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
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.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.room.powerlevels.Role
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.rx.mapOptional
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.platform.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
@ -140,6 +144,36 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction()
is RoomMemberProfileAction.VerifyUser -> prepareVerification()
is RoomMemberProfileAction.ShareRoomMemberProfile -> handleShareRoomMemberProfile()
is RoomMemberProfileAction.SetPowerLevel -> handleSetPowerLevel(action)
is RoomMemberProfileAction.BanOrUnbanUser -> handleBanOrUnbanAction(action)
is RoomMemberProfileAction.KickUser -> handleKickAction(action)
RoomMemberProfileAction.InviteUser -> handleInviteAction()
}
}
private fun handleSetPowerLevel(action: RoomMemberProfileAction.SetPowerLevel) = withState { state ->
if (room == null || action.previousValue == action.newValue) {
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))
} else if (action.askForValidation && state.isMine) {
_viewEvents.post(RoomMemberProfileViewEvents.ShowPowerLevelDemoteWarning(action.previousValue, action.newValue))
} else {
currentPowerLevelsContent.users[state.userId] = action.newValue
viewModelScope.launch {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
try {
awaitCallback<Unit> {
room.sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, null, currentPowerLevelsContent.toContent(), it)
}
_viewEvents.post(RoomMemberProfileViewEvents.OnSetPowerLevelSuccess)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
}
}
}
}
@ -156,15 +190,79 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
}
}
private fun handleInviteAction() {
if (room == null) {
return
}
viewModelScope.launch {
try {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
awaitCallback<Unit> {
room.invite(initialState.userId, callback = it)
}
_viewEvents.post(RoomMemberProfileViewEvents.OnInviteActionSuccess)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
}
}
}
private fun handleKickAction(action: RoomMemberProfileAction.KickUser) {
if (room == null) {
return
}
viewModelScope.launch {
try {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
awaitCallback<Unit> {
room.kick(initialState.userId, action.reason, it)
}
_viewEvents.post(RoomMemberProfileViewEvents.OnKickActionSuccess)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
}
}
}
private fun handleBanOrUnbanAction(action: RoomMemberProfileAction.BanOrUnbanUser) = withState { state ->
if (room == null) {
return@withState
}
val membership = state.asyncMembership() ?: return@withState
viewModelScope.launch {
try {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
awaitCallback<Unit> {
if (membership == Membership.BAN) {
room.unban(initialState.userId, action.reason, it)
} else {
room.ban(initialState.userId, action.reason, it)
}
}
_viewEvents.post(RoomMemberProfileViewEvents.OnBanActionSuccess)
} catch (failure: Throwable) {
_viewEvents.post(RoomMemberProfileViewEvents.Failure(failure))
}
}
}
private fun observeRoomMemberSummary(room: Room) {
val queryParams = roomMemberQueryParams {
this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE)
}
room.rx().liveRoomMembers(queryParams)
.map { it.firstOrNull()?.toMatrixItem().toOptional() }
.map { it.firstOrNull().toOptional() }
.unwrap()
.execute {
copy(userMatrixItem = it)
when (it) {
is Loading -> copy(userMatrixItem = Loading(), asyncMembership = Loading())
is Success -> copy(
userMatrixItem = Success(it().toMatrixItem()),
asyncMembership = Success(it().membership)
)
is Fail -> copy(userMatrixItem = Fail(it.error), asyncMembership = Fail(it.error))
is Uninitialized -> this
}
}
}
@ -184,17 +282,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.isUserAbleToKick(session.myUserId),
canBan = powerLevelsHelper.isUserAbleToBan(session.myUserId),
canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId),
canEditPowerLevel = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_POWER_LEVELS)
)
setState { copy(powerLevelsContent = it, actionPermissions = permissions) }
}.disposeOnClear()
roomSummaryLive.execute {
copy(isRoomEncrypted = it.invoke()?.isEncrypted == true)
}
powerLevelsContentLive.execute {
copy(powerLevelsContent = it)
}
Observable
.combineLatest(
roomSummaryLive,
@ -202,15 +305,11 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
BiFunction<RoomSummary, PowerLevelsContent, String> { roomSummary, powerLevelsContent ->
val roomName = roomSummary.toMatrixItem().getBestName()
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
val userPowerLevel = powerLevelsHelper.getUserPowerLevel(initialState.userId)
if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL) {
stringProvider.getString(R.string.room_member_power_level_admin_in, roomName)
} else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL) {
stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName)
} else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL) {
""
} else {
stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel, roomName)
when (val userPowerLevel = powerLevelsHelper.getUserRole(initialState.userId)) {
Role.Admin -> stringProvider.getString(R.string.room_member_power_level_admin_in, roomName)
Role.Moderator -> stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName)
Role.Default -> stringProvider.getString(R.string.room_member_power_level_default_in, roomName)
is Role.Custom -> stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel.value, roomName)
}
}
).execute {

View file

@ -21,6 +21,7 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
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
@ -31,13 +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 allDevicesAreCrossSignedTrusted: Boolean = false,
val asyncMembership: Async<Membership> = Uninitialized,
val actionPermissions: ActionPermissions = ActionPermissions()
) : MvRxState {
constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId)
constructor(args: RoomMemberProfileArgs) : this(userId = args.userId, roomId = args.roomId)
}
data class ActionPermissions(
val canKick: Boolean = false,
val canBan: Boolean = false,
val canInvite: Boolean = false,
val canEditPowerLevel: Boolean = false
)

View file

@ -0,0 +1,97 @@
/*
* 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.roommemberprofile.powerlevel
import android.app.Activity
import android.content.DialogInterface
import android.view.KeyEvent
import androidx.appcompat.app.AlertDialog
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_edit_power_level.view.*
object EditPowerLevelDialogs {
fun showChoice(activity: Activity, currentRole: Role, listener: (Int) -> Unit) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null)
dialogLayout.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId ->
dialogLayout.powerLevelCustomEditLayout.isVisible = checkedId == R.id.powerLevelCustomRadio
}
dialogLayout.powerLevelCustomEdit.setText(currentRole.value.toString())
when (currentRole) {
Role.Admin -> dialogLayout.powerLevelAdminRadio.isChecked = true
Role.Moderator -> dialogLayout.powerLevelModeratorRadio.isChecked = true
Role.Default -> dialogLayout.powerLevelDefaultRadio.isChecked = true
else -> dialogLayout.powerLevelCustomRadio.isChecked = true
}
AlertDialog.Builder(activity)
.setTitle(R.string.power_level_edit_title)
.setView(dialogLayout)
.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.powerLevelCustomEdit.text?.toString()?.toInt() ?: currentRole.value
}
}
listener(newValue)
}
.setNegativeButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener
{ dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.setOnDismissListener {
dialogLayout.hideKeyboard()
}
.create()
.show()
}
fun showValidation(activity: Activity, onValidate: () -> Unit) {
// Ask to the user the confirmation to upgrade.
AlertDialog.Builder(activity)
.setMessage(R.string.room_participants_power_level_prompt)
.setPositiveButton(R.string.yes) { _, _ ->
onValidate()
}
.setNegativeButton(R.string.no, null)
.show()
}
fun showDemoteWarning(activity: Activity, onValidate: () -> Unit) {
// Ask to the user the confirmation to downgrade his own role.
AlertDialog.Builder(activity)
.setTitle(R.string.room_participants_power_level_demote_warning_title)
.setMessage(R.string.room_participants_power_level_demote_warning_prompt)
.setPositiveButton(R.string.room_participants_power_level_demote) { _, _ ->
onValidate()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}

View file

@ -19,26 +19,35 @@ package im.vector.riotx.features.roomprofile
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.room.RequireActiveMembershipViewEvents
import im.vector.riotx.features.room.RequireActiveMembershipViewModel
import im.vector.riotx.features.room.RequireActiveMembershipViewState
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
import javax.inject.Inject
class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
class RoomProfileActivity :
VectorBaseActivity(),
ToolbarConfigurable,
RequireActiveMembershipViewModel.Factory {
companion object {
private const val EXTRA_ROOM_PROFILE_ARGS = "EXTRA_ROOM_PROFILE_ARGS"
fun newIntent(context: Context, roomId: String): Intent {
val roomProfileArgs = RoomProfileArgs(roomId)
return Intent(context, RoomProfileActivity::class.java).apply {
putExtra(EXTRA_ROOM_PROFILE_ARGS, roomProfileArgs)
putExtra(MvRx.KEY_ARG, roomProfileArgs)
}
}
}
@ -46,11 +55,25 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
private lateinit var sharedActionViewModel: RoomProfileSharedActionViewModel
private lateinit var roomProfileArgs: RoomProfileArgs
private val requireActiveMembershipViewModel: RequireActiveMembershipViewModel by viewModel()
@Inject
lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory
override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel {
return requireActiveMembershipViewModelFactory.create(initialState)
}
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun getLayoutRes() = R.layout.activity_simple
override fun initUiAndData() {
sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java)
roomProfileArgs = intent?.extras?.getParcelable(EXTRA_ROOM_PROFILE_ARGS) ?: return
roomProfileArgs = intent?.extras?.getParcelable(MvRx.KEY_ARG) ?: return
if (isFirstCreation()) {
addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs)
}
@ -64,6 +87,19 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
.disposeOnDestroy()
requireActiveMembershipViewModel.observeViewEvents {
when (it) {
is RequireActiveMembershipViewEvents.RoomLeft -> handleRoomLeft(it)
}
}
}
private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) {
if (roomLeft.leftMessage != null) {
Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show()
}
finish()
}
private fun openRoomUploads() {

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

@ -31,14 +31,15 @@ import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.android.api.session.room.powerlevels.Role
import im.vector.matrix.rx.asObservable
import im.vector.matrix.rx.mapOptional
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).isUserAbleToInvite(session.myUserId)
)
setState {
copy(actionsPermissions = permissions)
}
}.disposeOnClear()
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.unwrap()
@ -135,22 +149,22 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
roomMembers
.forEach { roomMember ->
val memberPowerLevel = powerLevelsHelper.getUserPowerLevel(roomMember.userId)
val userRole = powerLevelsHelper.getUserRole(roomMember.userId)
when {
roomMember.membership == Membership.INVITE -> invites.add(roomMember)
memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL -> admins.add(roomMember)
memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL -> moderators.add(roomMember)
memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL -> users.add(roomMember)
else -> customs.add(roomMember)
roomMember.membership == Membership.INVITE -> invites.add(roomMember)
userRole == Role.Admin -> admins.add(roomMember)
userRole == Role.Moderator -> moderators.add(roomMember)
userRole == Role.Default -> users.add(roomMember)
else -> customs.add(roomMember)
}
}
return listOf(
PowerLevelCategory.ADMIN to admins.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.CUSTOM to customs.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.INVITE to invites.sortedWith(roomMemberSummaryComparator),
PowerLevelCategory.USER to users.sortedWith(roomMemberSummaryComparator)
RoomMemberListCategories.ADMIN to admins.sortedWith(roomMemberSummaryComparator),
RoomMemberListCategories.MODERATOR to moderators.sortedWith(roomMemberSummaryComparator),
RoomMemberListCategories.CUSTOM to customs.sortedWith(roomMemberSummaryComparator),
RoomMemberListCategories.INVITE to invites.sortedWith(roomMemberSummaryComparator),
RoomMemberListCategories.USER to users.sortedWith(roomMemberSummaryComparator)
)
}

View file

@ -30,15 +30,20 @@ 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)
}
typealias RoomMemberSummaries = List<Pair<PowerLevelCategory, List<RoomMemberSummary>>>
data class ActionPermissions(
val canInvite: Boolean = false
)
enum class PowerLevelCategory(@StringRes val titleRes: Int) {
typealias RoomMemberSummaries = List<Pair<RoomMemberListCategories, List<RoomMemberSummary>>>
enum class RoomMemberListCategories(@StringRes val titleRes: Int) {
ADMIN(R.string.room_member_power_level_admins),
MODERATOR(R.string.room_member_power_level_moderators),
CUSTOM(R.string.room_member_power_level_custom),

View file

@ -134,7 +134,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text)
val input = layout.findViewById<EditText>(R.id.editText)
input.setText(deviceInfo.displayName)
AlertDialog.Builder(requireActivity())

View file

@ -154,7 +154,7 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
val canSend = if (powerLevelsContent == null) {
false
} else {
PowerLevelsHelper(powerLevelsContent).isAllowedToSend(isState, eventType, session.myUserId)
PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(session.myUserId, isState, eventType)
}
if (canSend) {
Timber.d("## canSendEvent() returns true")

View file

@ -112,7 +112,7 @@ class WidgetViewModel @AssistedInject constructor(@Assisted val initialState: Wi
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
.map {
PowerLevelsHelper(it).isAllowedToSend(true, null, session.myUserId)
PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, null)
}.subscribe {
setState { copy(canManageWidgets = it) }
}.disposeOnClear()

View file

@ -11,7 +11,7 @@
android:paddingRight="?dialogPreferredPadding">
<EditText
android:id="@+id/edit_text"
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text">

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -12,36 +13,36 @@
android:paddingBottom="12dp">
<TextView
android:id="@+id/deleteEventConfirmationText"
android:id="@+id/dialogConfirmationText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete_event_dialog_content"
tools:text="@string/delete_event_dialog_content"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/deleteEventReasonCheck"
android:id="@+id/dialogReasonCheck"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="true"
android:text="@string/delete_event_dialog_reason_checkbox"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deleteEventConfirmationText" />
app:layout_constraintTop_toBottomOf="@+id/dialogConfirmationText" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/deleteEventReasonTextInputLayout"
android:id="@+id/dialogReasonTextInputLayout"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/delete_event_dialog_reason_hint"
tools:hint="@string/delete_event_dialog_reason_hint"
app:counterEnabled="true"
app:counterMaxLength="240"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deleteEventReasonCheck">
app:layout_constraintTop_toBottomOf="@+id/dialogReasonCheck">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/deleteEventReasonInput"
android:id="@+id/dialogReasonInput"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<RadioGroup
android:id="@+id/powerLevelRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/powerLevelAdminRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_admin"
android:textColor="?riotx_text_primary" />
<RadioButton
android:id="@+id/powerLevelModeratorRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_moderator"
android:textColor="?riotx_text_primary" />
<RadioButton
android:id="@+id/powerLevelDefaultRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_default"
android:textColor="?riotx_text_primary" />
<RadioButton
android:id="@+id/powerLevelCustomRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_custom_no_value"
android:textColor="?riotx_text_primary" />
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/powerLevelCustomEditLayout"
style="@style/VectorTextInputLayout"
android:hint="@string/power_level_custom_no_value"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/powerLevelCustomEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number|numberSigned" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View file

@ -119,11 +119,13 @@
<im.vector.riotx.features.home.room.detail.widget.RoomWidgetsBannerView
android:id="@+id/roomWidgetsBannerView"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
tools:visibility="visible" />
<im.vector.riotx.core.ui.views.JumpToReadMarkerView
android:id="@+id/jumpToReadMarkerView"
@ -138,8 +140,7 @@
android:id="@+id/notificationAreaView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@ -166,6 +167,13 @@
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/badgeBarrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
<im.vector.riotx.core.platform.BadgeFloatingActionButton
android:id="@+id/jumpToBottomView"
android:layout_width="wrap_content"
@ -178,7 +186,7 @@
app:badgeTextColor="@color/white"
app:badgeTextPadding="2dp"
app:badgeTextSize="10sp"
app:layout_constraintBottom_toTopOf="@id/composerLayout"
app:layout_constraintBottom_toTopOf="@id/badgeBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:maxImageSize="16dp"
app:tint="@color/black" />

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

@ -949,9 +949,6 @@
<string name="room_participants_now">%1$s الآن</string>
<string name="room_participants_ago">%1$s منذ %2$s</string>
<string name="room_participants_action_unignore_prompt">أأعرض كل الرسائل من هذا المستخدم؟
سيُعيد هذا الإجراء تشغيل التطبيق وقد يأخذ بعض الوقت.</string>
<string name="room_participants_invite_join_names">"%1$s و "</string>
<string name="room_participants_invite_join_names_and">%1$s و %2$s</string>
<string name="room_participants_invite_join_names_combined">%1$s %2$s</string>

View file

@ -919,9 +919,6 @@
<string name="room_tombstone_continuation_description">Тази стая е продължение от предишна кореспонденция</string>
<string name="room_tombstone_predecessor_link">Натиснете тук за да видите по-стари съобщения</string>
<string name="room_participants_action_unignore_prompt">Показване на всички съобщения от този потребител?
Имайте предвид, че това действие ще рестартира приложението, което може да отнеме известно време.</string>
<string name="missing_permissions_error">Поради липсващи разрешения, това действие не е възможно.</string>
<plurals name="format_time_s">
<item quantity="one">1 сек.</item>
@ -1014,10 +1011,6 @@
<string name="settings_call_ringtone_dialog_title">Избор на мелодия за обаждания:</string>
<string name="room_participants_action_kick">Изгони</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Сигурни ли сте, че искате да изгоните този потребител от този чат?</item>
<item quantity="other">Сигурни ли сте, че искате да изгоните тези потребители от този чат?</item>
</plurals>
<string name="reason_hint">Причина</string>
<string name="settings_inline_url_preview_summary">Прегледи на връзки директно в самия чат, когато функцията се поддържа от сървъра.</string>

View file

@ -422,19 +422,12 @@
<string name="room_participants_action_set_admin">অ্যাডমিন কর</string>
<string name="room_participants_action_ignore">এই ব্যবহারকারীর কাছ থেকে সব বার্তা লুকান</string>
<string name="room_participants_action_unignore">এই ব্যবহারকারীর সব বার্তা দেখান</string>
<string name="room_participants_action_unignore_prompt">এই ব্যবহারকারীর কাছ থেকে সব বার্তা দেখান\?
\n
\nমনে রাখবেন যে এই পদক্ষেপটি অ্যাপ্লিকেশনটি পুনরায় চালু করবে এবং এটি কিছু সময় নিতে পারে।</string>
<string name="room_participants_invite_search_another_user">ব্যবহারকারী আইডি, নাম বা ইমেইল</string>
<string name="room_participants_action_mention">উল্লেখ</string>
<string name="room_participants_action_devices_list">সেশান তালিকা প্রদর্শন কর</string>
<string name="room_participants_power_level_prompt">আপনি এই পরিবর্তনটি পূর্বাবস্থায় ফিরিয়ে আনতে সক্ষম হবেন না যেহেতু আপনি ব্যবহারকারীকে একই শক্তি স্তর হিসাবে প্রচার করার জন্য প্রচার করছেন।
\nআপনি কি নিশ্চিত\?</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">আপনি কি নিশ্চিত যে এই চ্যাট থেকে এই ব্যবহারকারী কে পদাঘাত করতে চান\?</item>
<item quantity="other">আপনি কি নিশ্চিত যে এই চ্যাট থেকে এই ব্যবহারকারীদের কে পদাঘাত করতে চান\?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">আপনি কি এই চ্যাট থেকে এই ব্যবহারকারীকে নিষিদ্ধ করতে চান\?</string>
<string name="reason_hint">কারণ</string>

View file

@ -918,9 +918,6 @@ En voleu afegir algun?</string>
<string name="room_participants_now">Ara %1$s</string>
<string name="room_participants_ago">%1$s fa %2$s</string>
<string name="room_participants_action_unignore_prompt">Mostrar tots els missatges d\'aquest usuari?
Tingueu en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una estona.</string>
<string name="room_participants_invite_join_names">"%1$s,· "</string>
<string name="room_participants_invite_join_names_and">%1$s i %2$s</string>
<string name="room_participants_invite_join_names_combined">%1$s %2$s</string>
@ -1043,10 +1040,6 @@ Tingueu en compte que aquesta acció reiniciarà l\'aplicació i pot trigar una
<string name="settings_call_ringtone_dialog_title">Escolliu el to per les trucades:</string>
<string name="room_participants_action_kick">Expulsar</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Esteu segur que voleu expulsar aquest usuari d\'aquest xat?</item>
<item quantity="other">Esteu segur que voleu expulsar aquests usuaris d\'aquest xat?</item>
</plurals>
<string name="reason_hint">Motiu</string>
<string name="settings_inline_url_preview_summary">Mostra la vista prèvia dels enllaços dins del xat en cas que el vostre servidor base suporti aquesta funcionalitat.</string>

View file

@ -409,11 +409,6 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení.</string>
<string name="room_participants_action_devices_list">Zobrazit seznam relací</string>
<string name="room_preview_room_interactions_disabled">Toto je náhled místnosti. Interakce s místností byla vypnuta.</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Opravdu chcete vyhodit tohoto uživatele z této konverzace\?</item>
<item quantity="few">Opravdu chcete vyhodit tyto uživatele z této konverzace\?</item>
<item quantity="other">Opravdu chcete vyhodit tyto uživatele z této konverzace\?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">Opravdu chcete zakázat vstup tohoto uživatele do této konverzace\?</string>
<string name="reason_hint">Důvod</string>
@ -502,10 +497,7 @@ Vaši e-mailovou adresu můžete přidat k profilu v nastavení.</string>
<string name="room_preview_unlinked_email_warning">Pozvánka byla odeslána na %s, což není spárováno s tímto účtem.
\nPřihlaste se s jiným účtem nebo přidejte tento e-mail ke svému současnému účtu.</string>
<string name="room_preview_try_join_an_unknown_room">Snažíte se přistupovat k %s. Chcete vstoupit, abyste se mohli podílet na diskuzi\?</string>
<string name="room_participants_action_unignore_prompt">Zobrazit všechny zprávy od tohoto uživatele\?
\n
\nTato akce provede restart aplikace a může nějakou dobu trvat.</string>
<string name="room_participants_power_level_prompt">Tuto změnu nelze vrátit, protože povyšujete uživatele na stejnou úroveň, jakou máte vy.
<string name="room_participants_power_level_prompt">Tuto změnu nelze vrátit, protože povyšujete uživatele na stejnou úroveň, jakou máte vy.
\nOpravdu to chcete udělat\?</string>
<string name="people_search_invite_by_id_dialog_description">Prosím zadejte jednu nebo více e-mailových adres nebo Matrix ID</string>

View file

@ -975,9 +975,6 @@ Du kannst sie jetzt aktivieren oder später über das Einstellungsmenü.</string
<string name="room_tombstone_continuation_description">Dieser Raum ist die Fortsetzung einer anderen Konversation</string>
<string name="room_tombstone_predecessor_link">Klicke hier um die älteren Nachrichten zu sehen</string>
<string name="room_participants_action_unignore_prompt">Zeige alle Nachrichten dieses Benutzers?
Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.</string>
<string name="missing_permissions_error">Nicht berechtigt, diese Aktion durchzuführen.</string>
<plurals name="format_time_s">
<item quantity="one">1s</item>
@ -1061,10 +1058,6 @@ Beachte: Diese Aktion wird die App neu starten und einige Zeit brauchen.</string
<string name="call_anyway">Trotzdem anrufen</string>
<string name="room_participants_action_kick">Kicken</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Bist du sicher, dass du diesen Benutzer aus diesem Chat kicken möchtest?</item>
<item quantity="other">Bist du sicher, dass du diese Benutzer aus diesem Chat kicken möchtest?</item>
</plurals>
<string name="reason_hint">Grund</string>
<string name="settings_inline_url_preview_summary">Im Chat Linkvorschau aktivieren, wenn dein Heimserver diese Funktion unterstützt.</string>

View file

@ -565,9 +565,6 @@
<string name="room_participants_action_set_admin">Igi administranto</string>
<string name="room_participants_action_ignore">Kaŝi ĉiujn mesaĝojn de ĉi tiu uzanto</string>
<string name="room_participants_action_unignore">Remontri ĉiujn mesaĝojn de ĉi tiu uzanto</string>
<string name="room_participants_action_unignore_prompt">Ĉu remontri ĉiujn mesaĝojn de ĉi tiu uzanto\?
\n
\nRimarku, ke tiu ĉi ago reekigos la aplikaĵon, kio povas daŭri ioman temon.</string>
<string name="room_participants_invite_search_another_user">Identigilo de uzanto, nomo, aŭ retpoŝtadreso</string>
<string name="room_participants_action_mention">Mencii</string>
<string name="room_participants_action_devices_list">Montri liston de salutaĵoj</string>

View file

@ -961,9 +961,6 @@ La visibilidad de mensajes en Matrix es similar a la del correo electrónico. Qu
<string name="lock_screen_hint">Escribe aquí…</string>
<string name="send_bug_report_description_in_english">Si es posible, por favor escribe la descripción en inglés.</string>
<string name="room_participants_action_unignore_prompt">¿Mostrar todos los mensajes de este usuario?
Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de tiempo.</string>
<string name="room_message_placeholder_reply_to_encrypted">Enviar una respuesta cifrada…</string>
<string name="room_message_placeholder_reply_to_not_encrypted">Enviar una respuesta (sin cifrar)…</string>
<string name="settings_without_flair">Actualmente no eres miembro de ninguna comunidad.</string>
@ -1074,10 +1071,6 @@ Ten en cuenta que esta acción reiniciará la aplicación y puede tardar algo de
<string name="video_call_in_progress">Llamada de video en proceso…</string>
<string name="room_participants_action_kick">Expulsar</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">¿Seguro que quieres expulsar este usuario de esta conversación?</item>
<item quantity="other">¿Seguro que quieres expulsar estos usuarios de esta conversación?</item>
</plurals>
<string name="reason_hint">Razón</string>
<string name="settings_notification_troubleshoot">Diagnóstico de fallas</string>

View file

@ -898,9 +898,6 @@ Matrix-eko mezuen ikusgaitasuna e-mail sistemaren antekoa da. Guk zure mezuak ah
<string name="error_empty_field_your_password">Sartu zure pasahitza.</string>
<string name="send_bug_report_description_in_english">Ahal dela idatzi deskripzioa ingelesez.</string>
<string name="room_participants_action_unignore_prompt">Erakutsi erabiltzaile honen mezu guztiak?
Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat beharko lukeela.</string>
<string name="room_message_placeholder_reply_to_encrypted">Zifratutako erantzuna bidalita…</string>
<string name="room_message_placeholder_reply_to_not_encrypted">Bidali erantzuna (zifratu gabea)…</string>
<string name="settings_preview_media_before_sending">Aurreikusi multimedia bidali aurretik</string>
@ -1015,10 +1012,6 @@ Kontuan izan ekintza honek aplikazioa berrabiaraziko duela eta denbora bat behar
<string name="settings_call_ringtone_dialog_title">Hautatu deientzako doinua:</string>
<string name="room_participants_action_kick">Kanporatu</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Ziur erabiltzaile hau txat honetatik kanporatu nahi duzula?</item>
<item quantity="other">Ziur erabiltzaile hauek txat honetatik kanporatu nahi dituzula?</item>
</plurals>
<string name="reason_hint">Arrazoia</string>
<string name="settings_inline_url_preview_summary">Erakutsi txateko esteken aurrebista hasiera-zerbitzariak onartzen badu.</string>

View file

@ -959,13 +959,6 @@ Haluatko lisätä paketteja?</string>
<string name="room_participants_ago">Oli %1$s %2$s sitten</string>
<string name="room_participants_action_kick">Poista huoneesta</string>
<string name="room_participants_action_unignore_prompt">Näytä tämän käyttäjän kaikki viestit\?
\n
\nHuomioi, että tämä toiminto käynnistää Riotin uudelleen ja siinä voi kestää jonkin aikaa.</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Oletko varma, että haluat poistaa tämän käyttäjän tästä keskustelusta\?</item>
<item quantity="other">Oletko varma, että haluat poistaa nämä käyttäjät tästä keskustelusta\?</item>
</plurals>
<string name="reason_hint">Syy</string>
<string name="room_participants_invite_join_names">"%1$s, "</string>

View file

@ -906,9 +906,6 @@ Voulez-vous en ajouter ?</string>
<string name="error_empty_field_your_password">Veuillez renseigner votre mot de passe.</string>
<string name="send_bug_report_description_in_english">Si possible, veuillez écrire la description en anglais.</string>
<string name="room_participants_action_unignore_prompt">Afficher tous les messages de cet utilisateur ?
Veuillez noter que cette action redémarrera lapplication et pourra prendre un certain temps.</string>
<string name="room_message_placeholder_reply_to_encrypted">Envoyer une réponse chiffrée…</string>
<string name="room_message_placeholder_reply_to_not_encrypted">Envoyer une réponse (non chiffrée)…</string>
<string name="settings_preview_media_before_sending">Prévisualiser le média avant de lenvoyer</string>
@ -1018,10 +1015,6 @@ Veuillez noter que cette action redémarrera lapplication et pourra prendre u
<string name="call_anyway">Appeler quand même</string>
<string name="room_participants_action_kick">Expulser</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one" tools:ignore="ImpliedQuantity">Voulez-vous vraiment expulser cet utilisateur de cette discussion ?</item>
<item quantity="other">Voulez-vous vraiment expulser ces utilisateurs de cette discussion ?</item>
</plurals>
<string name="reason_hint">Motif</string>
<string name="settings_inline_url_preview_summary">Afficher un aperçu des liens dans la discussion quand votre serveur daccueil le permet.</string>

View file

@ -577,16 +577,9 @@
<string name="settings_add_phone_number">Dodaj broj telefona</string>
<string name="settings_app_info_link_title">Informacije o aplikaciji</string>
<string name="settings_app_info_link_summary">Prikaži informacije o aplikaciji u postavkama sustava.</string>
<string name="room_participants_action_unignore_prompt">Želite li prikazati sve poruke ovog korisnika\?
\nOva će radnja ponovno pokrenuti aplikaciju i to bi moglo potrajati.</string>
<string name="room_participants_power_level_prompt">Nećete moći poništiti ovu izmjenu jer dotičnom korisniku dajete istu razinu upravljanja kao Vaša.
\nJeste li sigurni\?</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Jeste li sigurni da želite izbaciti ovog korisnika iz razgovora\?</item>
<item quantity="few">Jeste li sigurni da želite izbaciti ove korisnike iz razgovora\?</item>
<item quantity="other">Jeste li sigurni da želite izbaciti ove korisnike iz razgovora\?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">Jeste li sigurni da želite zabraniti korisniku pristup ovom razgovoru\?</string>
<string name="room_participants_invite_prompt_msg">Jeste li sigurni da želite pozvati %s u ovoj razgovor\?</string>
<string name="people_search_invite_by_id_dialog_description">Unesite jednu ili više adresa e-pošte ili identitet u Matrixu</string>

View file

@ -901,9 +901,6 @@ Matrixban az üzenetek láthatósága hasonlít az e-mailre. Az üzenet törlés
<string name="error_empty_field_your_password">Kérlek add meg a jelszavad.</string>
<string name="send_bug_report_description_in_english">Kérlek, ha lehetséges a leírást angolul írd.</string>
<string name="room_participants_action_unignore_prompt">Mutassuk ennek a felhasználónak minden üzenetét?
Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe.</string>
<string name="room_message_placeholder_reply_to_encrypted">Titkosított válasz küldése…</string>
<string name="room_message_placeholder_reply_to_not_encrypted">Válasz küldése (titkosítás nélkül)…</string>
<string name="settings_preview_media_before_sending">Média előnézete küldés előtt</string>
@ -1013,10 +1010,6 @@ Vedd figyelembe, hogy az alkalmazás újraindul ami sok időt vehet igénybe.</s
<string name="call_anyway">Hívás mindenképpen</string>
<string name="room_participants_action_kick">Elküld</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Biztos, hogy kirúgod ezt a felhasználót\?</item>
<item quantity="other">Biztos, hogy kirúgod ezeket a felhasználókat\?</item>
</plurals>
<string name="reason_hint">Ok</string>
<string name="settings_inline_url_preview_summary">URL előnézet a csevegő ablakban, ha a Matrix szervered támogatja ezt a lehetőséget.</string>

View file

@ -400,9 +400,6 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda
<string name="room_participants_action_set_admin">Jadikan admin</string>
<string name="room_participants_action_ignore">Sembunyikan semua pesan dari pengguna ini</string>
<string name="room_participants_action_unignore">Tunjukkan semua pesan dari pengguna ini</string>
<string name="room_participants_action_unignore_prompt">Tunjukkan semua pesan dari pengguna ini?
Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.</string>
<string name="room_participants_invite_search_another_user">ID Pengguna, Nama atau email</string>
<string name="room_participants_action_mention">Sebut</string>
<string name="room_participants_action_devices_list">Tunjukkan Daftar Perangkat</string>
@ -965,9 +962,6 @@ Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.</strin
<string name="video_call_in_progress">Panggilan Video Sedang Berlangsung…</string>
<string name="room_participants_action_kick">Keluarkan</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="other">Apakah Anda yakin ingin mengeluarkan pengguna-pengguna tersebut dari percakapan ini?</item>
</plurals>
<string name="reason_hint">Alasan</string>
<string name="room_sliding_menu_version_x">Versi %s</string>

View file

@ -400,9 +400,6 @@ Anda mungkin ingin masuk dengan akun lain, atau tambahkan email ini ke akun Anda
<string name="room_participants_action_set_admin">Jadikan admin</string>
<string name="room_participants_action_ignore">Sembunyikan semua pesan dari pengguna ini</string>
<string name="room_participants_action_unignore">Tunjukkan semua pesan dari pengguna ini</string>
<string name="room_participants_action_unignore_prompt">Tunjukkan semua pesan dari pengguna ini?
Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.</string>
<string name="room_participants_invite_search_another_user">ID Pengguna, Nama atau email</string>
<string name="room_participants_action_mention">Sebut</string>
<string name="room_participants_action_devices_list">Tunjukkan Daftar Perangkat</string>
@ -965,9 +962,6 @@ Tindakan ini akan memulai ulang aplikasi dan mungkin cukup memakan waktu.</strin
<string name="video_call_in_progress">Panggilan Video Sedang Berlangsung…</string>
<string name="room_participants_action_kick">Keluarkan</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="other">Apakah Anda yakin ingin mengeluarkan pengguna-pengguna tersebut dari percakapan ini?</item>
</plurals>
<string name="reason_hint">Alasan</string>
<string name="room_sliding_menu_version_x">Versi %s</string>

View file

@ -983,9 +983,6 @@
<string name="room_tombstone_continuation_description">Questa stanza contiene una conversazione cominciata altrove</string>
<string name="room_tombstone_predecessor_link">Clicca qui per vedere i messaggi precedenti</string>
<string name="room_participants_action_unignore_prompt">Mostrare tutti i messaggi di questo utente\?
\n
\nTieni presente che questa azione riavvierà l\'app e ciò potrebbe richiedere molto tempo.</string>
<string name="missing_permissions_error">Non hai permessi sufficienti per effettuare questa azione.</string>
<plurals name="format_time_s">
<item quantity="one">1s</item>
@ -1069,10 +1066,6 @@
<string name="call_anyway">Chiama comunque</string>
<string name="room_participants_action_kick">Butta fuori</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Sei sicuro di voler buttare fuori questo utente\?</item>
<item quantity="other">Sei sicuro di voler buttare fuori questi utenti\?</item>
</plurals>
<string name="reason_hint">Motivo</string>
<string name="settings_inline_url_preview_summary">Se il tuo Home Server supporta questa funzione, all\'interno delle chat verrà visualizzata un\'anteprima dei link postati.</string>

View file

@ -848,9 +848,6 @@ Riotアプリがあなたの電話帳へアクセスすることを許可しま
<string name="room_participants_now">現在 %1$s</string>
<string name="room_participants_ago">%2$s 前 %1$s</string>
<string name="room_participants_action_unignore_prompt">このユーザからのすべてのメッセージを表示しますか?
この操作によってアプリを再起動するため、しばらく時間がかかることにご注意ください。</string>
<string name="room_participants_invite_join_names">"%1$s、 "</string>
<string name="room_participants_invite_join_names_and">%1$s と %2$s</string>
<string name="room_participants_invite_join_names_combined">%1$s %2$s</string>
@ -999,9 +996,6 @@ Matrixでのメッセージの可視性は電子メールと同様です。メ
<string name="settings_call_ringtone_dialog_title">着信音を選んでください:</string>
<string name="room_participants_action_kick">追い出す</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="other">このチャットから本当にこのユーザーを追い出しますか?</item>
</plurals>
<string name="reason_hint">理由</string>
<string name="notification_sync_init">サービスを初期化</string>

View file

@ -420,18 +420,12 @@
<string name="room_participants_action_set_admin">관리자로 하기</string>
<string name="room_participants_action_ignore">이 사용자의 모든 메시지 숨기기</string>
<string name="room_participants_action_unignore">이 사용자의 모든 메시지 보이기</string>
<string name="room_participants_action_unignore_prompt">이 사용자의 모든 메시지를 보이겠습니까\?
\n
\n이 동작은 앱을 재시작하며 일정 시간이 걸릴 수 있습니다.</string>
<string name="room_participants_invite_search_another_user">사용자 ID, 이름 혹은 이메일</string>
<string name="room_participants_action_mention">언급</string>
<string name="room_participants_action_devices_list">기기 목록 보이기</string>
<string name="room_participants_power_level_prompt">사용자를 자신과 동일한 권한 등급으로 승격시키는 것은 취소할 수 없습니다.
\n확신합니까\?</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="other">이 사용자들을 이 대화에서 추방하겠습니까\?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">이 사용자를 이 대화에서 출입 금지하겠습니까\?</string>
<string name="reason_hint">이유</string>

View file

@ -973,9 +973,6 @@
<string name="settings_labs_keyboard_options_to_send_message">Enter-knop van toetsenbord gebruiken om berichten te versturen</string>
<string name="command_description_emote">Toont een actie</string>
<string name="room_participants_action_unignore_prompt">Alle berichten van deze gebruiker tonen\?
\n
\nLet op: de app zal voor deze actie opnieuw opgestart worden; dit kan even duren.</string>
<string name="command_description_ban_user">Verbant gebruiker met gegeven ID</string>
<string name="command_description_unban_user">Heft verbanning van gebruiker met gegeven ID op</string>
<string name="command_description_op_user">Stel het machtsniveau van een gebruiker in</string>
@ -1063,10 +1060,6 @@
<string name="settings_call_ringtone_dialog_title">Selecteer beltoon voor oproepen:</string>
<string name="room_participants_action_kick">Eruit sturen</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Weet u zeker dat u deze gebruiker uit dit gesprek wilt sturen\?</item>
<item quantity="other">Weet u zeker dat u deze gebruikers uit dit gesprek wilt sturen\?</item>
</plurals>
<string name="reason_hint">Reden</string>
<string name="room_sliding_menu_version_x">Versie %s</string>

View file

@ -361,9 +361,6 @@
<string name="room_participants_action_set_admin">Gjer til administrator</string>
<string name="room_participants_action_ignore">Gøym alle meldingar frå denne brukaren</string>
<string name="room_participants_action_unignore">Syn alle meldingar frå denne brukaren</string>
<string name="room_participants_action_unignore_prompt">Syn alle meldingar frå denne brukaren\?
\n
\nMerk deg at denne handlinga startar programmet på nytt og kan ta litt tid.</string>
<string name="room_participants_invite_search_another_user">Brukar-ID, namn, eller e-post</string>
<string name="room_participants_action_mention">Nemn</string>
<string name="room_participants_action_devices_list">Vis sesjonsliste</string>
@ -972,10 +969,6 @@ Meldingssynlegheit på Matrix liknar på epost. At vi gløymer meldingane dine t
<string name="video_call_in_progress">Ein videosamtale pågår…</string>
<string name="room_participants_action_kick">Spark</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Er du trygg på at du vil sparka denne brukaren frå samtala\?</item>
<item quantity="other">Er du trygg på at du vil sparka desse brukarane frå samtala\?</item>
</plurals>
<string name="reason_hint">Grunn</string>
<string name="settings_notification_advanced">Avanserte varslingsinnstillingar</string>

View file

@ -961,8 +961,6 @@ Widoczność wiadomości w Matrix jest podobna do wiadomości e-mail. Nasze zapo
<item quantity="many" />
</plurals>
<string name="room_participants_action_unignore_prompt">Pokazać wszystkie wiadomości od tego użytkownika?
Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas.</string>
<plurals name="room_details_selected">
<item quantity="one">1 zaznaczone</item>
<item quantity="few">%d zaznaczone</item>
@ -1035,11 +1033,6 @@ Pamiętaj ta akcja może zresetować aplikacje i potrwać jakiś czas.</string>
<string name="call_anyway">Zadzwoń mimo to</string>
<string name="settings_call_category">Połączenia</string>
<string name="room_participants_action_kick">Wyrzuć</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Czy chcesz wyrzucić tego użytkownika z rozmowy\?</item>
<item quantity="few">Czy chcesz wyrzucić tych użytkowników z rozmowy\?</item>
<item quantity="many" />
</plurals>
<string name="settings_send_markdown">Formatowanie Markdown</string>
<string name="settings_show_join_leave_messages">Pokaż zdarzenia dołączenia i wyjścia</string>
<string name="show_info_area_messages_and_errors">Dla komunikatów i błędów</string>

View file

@ -930,9 +930,6 @@ Quer adicionar alguns agora?</string>
<string name="room_participants_now">%1$s agora</string>
<string name="room_participants_ago">há %1$s %2$s</string>
<string name="room_participants_action_unignore_prompt">Exibir todas as mensagens desta(e) usuária(o)?
Note que esta ação vai reiniciar o aplicativo e isso pode tomar um certo tempo.</string>
<string name="room_participants_invite_join_names">"%1$s, "</string>
<string name="room_participants_invite_join_names_and">%1$s e %2$s</string>
<string name="room_participants_invite_join_names_combined">%1$s %2$s</string>
@ -1070,10 +1067,6 @@ Por favor revise as configurações da conta.</string>
<string name="settings_troubleshoot_test_device_settings_title">Configurações do Dispositivo.</string>
<string name="settings_troubleshoot_test_device_settings_success">Notificações estão habilitadas para este dispositivo.</string>
<string name="room_participants_action_kick">Desconectar</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Você tem certeza que deseja desconectar este usuário deste chat?</item>
<item quantity="other">Você tem certeza que deseja desconectar estes usuários deste chat?</item>
</plurals>
<string name="settings_troubleshoot_test_device_settings_failed">Notificações não são permitidas para este dispositivo.
Por favor revise as configurações do Riot.</string>
<string name="settings_troubleshoot_test_device_settings_quickfix">Habilitar</string>

View file

@ -835,9 +835,6 @@ Adicionar alguns agora?</string>
<string name="room_participants_now">%1$s agora</string>
<string name="room_participants_ago">há %1$s %2$s</string>
<string name="room_participants_action_unignore_prompt">Mostrar todas as mensagens deste utilizador?
Note que esta acção reiniciará a aplicação e poderá demorar algum tempo.</string>
<string name="room_participants_ban_prompt_msg">Tem a certeza que pretende banir este utilizador desta conversa?</string>
<string name="room_participants_invite_join_names">"%1$s, "</string>

View file

@ -985,9 +985,6 @@
<string name="command_description_ban_user">Банит пользователя с указанным ID</string>
<string name="command_description_unban_user">Разбанит пользователя с указанным ID</string>
<string name="command_description_op_user">Определение уровня доступа пользователя</string>
<string name="room_participants_action_unignore_prompt">Показать все сообщения от этого пользователя?
Учтите, что это действие перезапустит приложение и может занять некоторое время.</string>
<string name="command_description_deop_user">Сбросить уровень доступа для пользователя с данным ID</string>
<string name="command_description_invite_user">Пригласить пользователя с данным ID в текущую комнату</string>
<string name="command_description_part_room">Покинуть комнату</string>
@ -1096,11 +1093,6 @@
<string name="video_call_in_progress">Идёт видеозвонок …</string>
<string name="room_participants_action_kick">Выгнать</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Вы уверены, что хотите выгнать этого пользователя из чата\?</item>
<item quantity="few">Вы уверены, что хотите выгнать этих пользователей из чата\?</item>
<item quantity="many">Вы уверены, что хотите выгнать этих пользователей из чата\?</item>
</plurals>
<string name="reason_hint">Причина</string>
<string name="settings_notification_troubleshoot">Поиск проблем с уведомлениями</string>

View file

@ -948,9 +948,6 @@ Viditeľnosť správ odoslaných cez matrix funguje podobne ako viditeľnosť sp
<string name="room_participants_now">teraz %1$s</string>
<string name="room_participants_ago">%1$s pred %2$s</string>
<string name="room_participants_action_unignore_prompt">Zobraziť všetky správy od tohoto používateľa?
Pozor! Vykonaním tejto akcie reštartujete aplikáciu a opätovné načítanie môže chvíľu trvať.</string>
<string name="room_participants_invite_join_names">"%1$s, "</string>
<string name="room_participants_invite_join_names_and">%1$s a %2$s</string>
<string name="room_participants_invite_join_names_combined">%1$s %2$s</string>
@ -1056,11 +1053,6 @@ Pozor! Vykonaním tejto akcie reštartujete aplikáciu a opätovné načítanie
<string name="video_call_in_progress">Prebiehajúci video hovor…</string>
<string name="room_participants_action_kick">Vykázať</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Ste si istí, že chcete z miestnosti vykázať tohto používateľa\?</item>
<item quantity="few">Ste si istí, že chcete z miestnosti vykázať týchto používateľov\?</item>
<item quantity="other">Ste si istí, že chcete z miestnosti vykázať týchto používateľov\?</item>
</plurals>
<string name="reason_hint">Dôvod</string>
<string name="settings_notification_troubleshoot">Riešiť problémy s oznámeniami</string>

View file

@ -284,10 +284,6 @@
<string name="room_participants_invite_search_another_user">ID Përdoruesi, Emër ose email-i</string>
<string name="room_participants_action_mention">Përmendje</string>
<string name="room_participants_action_devices_list">Shfaq Listë Sesionesh</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Jeni i sigurt se doni të përzihet ky përdorues nga kjo fjalosje?</item>
<item quantity="other">Jeni i sigurt se doni të përzihen këta përdorues nga kjo fjalosje?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">Jeni i sigurt se doni të dëbohet ky përdorues nga kjo fjalosje?</string>
<string name="reason_hint">Arsye</string>
@ -870,9 +866,6 @@
<item quantity="one">1 anëtar</item>
<item quantity="other">%d anëtarë</item>
</plurals>
<string name="room_participants_action_unignore_prompt">Të shfaqen krejt mesazhet nga ky përdorues\?
\n
\nKini parasysh që ky veprim do të sjellë rinisjen e aplikacionit dhe mund të hajë ca kohë.</string>
<string name="room_participants_power_level_prompt">Sdo të jeni në gjendje ta zhbëni këtë ndryshim, ngaqë po e promovoni përdoruesin të ketë të njëjtën shkallë pushteti si ju vetë.
\nJeni i sigurt\?</string>

View file

@ -448,19 +448,12 @@ Eğer yeni kurtarma yöntemini siz ayarlamadıysanız, bir saldırgan hesabını
<string name="room_participants_action_set_admin">Yönetici yap</string>
<string name="room_participants_action_ignore">Bu kullanıcının tüm mesajlarını gizle</string>
<string name="room_participants_action_unignore">Bu kullanıcının tüm mesajlarını göster</string>
<string name="room_participants_action_unignore_prompt">Bu kullanıcının tüm mesajlarını göster
\n
\nBu işlem uygulamayı yeniden başlatak ve biraz zaman alacak.</string>
<string name="room_participants_invite_search_another_user">Kullanıcı ID, Ad ya da eposta</string>
<string name="room_participants_action_mention">Bahset</string>
<string name="room_participants_action_devices_list">Oturum Listesini Göster</string>
<string name="room_participants_power_level_prompt">Bu işlemin geri dönüşü yok kullanıcıyı sen ile aynı erişim seviyesine getiriyorsun.
\nEmin misin\?</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Kullanıcıyı bu sohbetten atmak istediğine emin misin\?</item>
<item quantity="other">Kullanıcıları bu sohbetten atmak istediğine emin misin\?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">Kullanıcıyı bu sohbetten engellemek istediğine emin misin\?</string>
<string name="reason_hint">Neden</string>

View file

@ -893,9 +893,6 @@
<string name="room_participants_now">Зараз %1$s</string>
<string name="room_participants_ago">%1$s %2$s тому</string>
<string name="room_participants_action_unignore_prompt">Показати усі повідомлення від цього користувача?
Зауважте, що це перезавантажить додаток та може зайняти деякий час.</string>
<string name="room_participants_invite_join_names">"%1$s, "</string>
<string name="room_participants_invite_join_names_and">%1$s та %2$s</string>
<string name="room_participants_invite_join_names_combined">%1$s %2$s</string>
@ -1088,12 +1085,6 @@
<string name="call_anyway">Все одно подзвонити</string>
<string name="room_participants_action_kick">Копнути</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Ви певні, що хочете викинути цього (%d) користувача з чату?</item>
<item quantity="few">Ви певні, що хочете викинути цих користувачів з чату?</item>
<item quantity="many">Ви певні, що хочете викинути цих користувачів з чату?</item>
<item quantity="other" />
</plurals>
<string name="reason_hint">Причина</string>
<string name="settings_inline_url_preview_summary">Попередній перегляд посилань у чаті, у разі якщо Ваш сервер підтримує таку можливість.</string>

View file

@ -894,9 +894,6 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意
<string name="speak">发言</string>
<string name="send_bug_report_description_in_english">如果可能的话,请使用英文撰写问题描述。</string>
<string name="room_participants_action_unignore_prompt">显示来自该用户的所有信息?
注意,此操作会重启应用并将花费一些时间。</string>
<string name="room_message_placeholder_reply_to_encrypted">发送加密的回复…</string>
<string name="room_message_placeholder_reply_to_not_encrypted">发送回复(未加密)…</string>
<string name="settings_preview_media_before_sending">发送前预览媒体文件</string>
@ -991,9 +988,6 @@ Matrix 中的消息可见性类似于电子邮件。我们忘记您的消息意
<string name="settings_call_ringtone_dialog_title">请选择来电铃声:</string>
<string name="room_participants_action_kick">移除</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="other">您确定要从此聊天室中移除这些用户吗?</item>
</plurals>
<string name="reason_hint">理由</string>
<string name="room_sliding_menu_version_x">版本 %s</string>

View file

@ -867,9 +867,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
<string name="error_empty_field_your_password">請輸入您的密碼。</string>
<string name="send_bug_report_description_in_english">如果可以,請使用英文撰寫描述。</string>
<string name="room_participants_action_unignore_prompt">顯示所有從此使用者而來的訊息?
注意此動作將會重新啟動應用程式,而其可能需要一點時間。</string>
<string name="room_message_placeholder_reply_to_encrypted">傳送加密回覆……</string>
<string name="room_message_placeholder_reply_to_not_encrypted">傳送回覆(未加密)……</string>
<string name="settings_preview_media_before_sending">在傳送前預覽媒體</string>
@ -962,9 +959,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
<string name="call_anyway">無論如何都要通話</string>
<string name="room_participants_action_kick">踢出</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="other">您確定您想要從這個聊天中踢出這些使用者嗎?</item>
</plurals>
<string name="reason_hint">理由</string>
<string name="settings_inline_url_preview_summary">當您的家伺服器支援此功能時在聊天中預覽連結。</string>

View file

@ -465,6 +465,7 @@
<string name="room_participants_header_devices">SESSIONS</string>
<string name="room_participants_action_invite">Invite</string>
<string name="room_participants_action_cancel_invite">Cancel invite</string>
<string name="room_participants_action_leave">Leave this room</string>
<string name="room_participants_action_remove">Remove from this room</string>
<string name="room_participants_action_ban">Ban</string>
@ -473,19 +474,35 @@
<string name="room_participants_action_set_default_power_level">Reset to normal user</string>
<string name="room_participants_action_set_moderator">Make moderator</string>
<string name="room_participants_action_set_admin">Make admin</string>
<string name="room_participants_action_ignore">Hide all messages from this user</string>
<string name="room_participants_action_unignore">Show all messages from this user</string>
<string name="room_participants_action_unignore_prompt">Show all messages from this user?\n\nNote that this action will restart the app and it may take some time.</string>
<string name="room_participants_invite_search_another_user">User ID, Name or email</string>
<string name="room_participants_action_mention">Mention</string>
<string name="room_participants_action_devices_list">Show Session List</string>
<string name="room_participants_power_level_prompt">You will not be able to undo this change as you are promoting the user to have the same power level as yourself.\nAre you sure?</string>
<plurals name="room_participants_kick_prompt_msg">
<item quantity="one">Are you sure that you want to kick this user from this chat?</item>
<item quantity="other">Are you sure that you want to kick these users from this chat?</item>
</plurals>
<string name="room_participants_ban_prompt_msg">Are you sure that you want to ban this user from this chat?</string>
<string name="room_participants_power_level_demote_warning_title">Demote yourself?</string>
<string name="room_participants_power_level_demote_warning_prompt">"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges."</string>
<string name="room_participants_power_level_demote">Demote</string>
<string name="room_participants_action_ignore_title">Ignore user</string>
<string name="room_participants_action_ignore_prompt_msg">Ignoring this user will remove their messages from rooms you share.\n\nYou can reverse this action at any time in the general settings.</string>
<string name="room_participants_action_ignore">Ignore</string>
<string name="room_participants_action_unignore_title">Unignore user</string>
<string name="room_participants_action_unignore_prompt_msg">Unignoring this user will show all messages from them again.</string>
<string name="room_participants_action_unignore">Unignore</string>
<string name="room_participants_action_cancel_invite_title">Cancel invite</string>
<string name="room_participants_action_cancel_invite_prompt_msg">Are you sure you want to cancel the invite for this user?</string>
<string name="room_participants_kick_title">Kick user</string>
<string name="room_participants_kick_reason">Reason to kick</string>
<string name="room_participants_kick_prompt_msg">kicking user will remove them from this room.\n\nTo prevent them from joining again, you should ban them instead.</string>
<string name="room_participants_ban_title">Ban user</string>
<string name="room_participants_ban_reason">Reason to ban</string>
<string name="room_participants_unban_title">Unban user</string>
<string name="room_participants_ban_prompt_msg">Banning user will kick them from this room and prevent them from joining again.</string>
<string name="room_participants_unban_prompt_msg">Unbanning user will allow them to join the room again.</string>
<string name="reason_hint">Reason</string>
<!--
@ -2068,6 +2085,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="room_profile_section_security">Security</string>
<string name="room_profile_section_security_learn_more">Learn more</string>
<string name="room_profile_section_more">More</string>
<string name="room_profile_section_admin">Admin Actions</string>
<string name="room_profile_section_more_settings">Room settings</string>
<string name="room_profile_section_more_notifications">Notifications</string>
<plurals name="room_profile_section_more_member_list">
@ -2086,6 +2104,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="room_member_power_level_admin_in">Admin in %1$s</string>
<string name="room_member_power_level_moderator_in">Moderator in %1$s</string>
<string name="room_member_power_level_default_in">Default in %1$s</string>
<string name="room_member_power_level_custom_in">Custom (%1$d) in %2$s</string>
<string name="room_member_jump_to_read_receipt">Jump to read receipt</string>
@ -2430,5 +2449,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">Set role</string>
</resources>