mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 20:29:10 +03:00
Merge pull request #1459 from vector-im/feature/power_level
Feature/power level
This commit is contained in:
commit
54b3af2c88
88 changed files with 1661 additions and 632 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotx.core.extensions.canReact
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Quick reactions state
|
||||
*/
|
||||
data class ToggleState(
|
||||
val reaction: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
data class ActionPermissions(
|
||||
val canSendMessage: Boolean = false,
|
||||
val canReact: Boolean = false,
|
||||
val canRedact: Boolean = false
|
||||
)
|
||||
|
||||
data class MessageActionState(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val informationData: MessageInformationData,
|
||||
val timelineEvent: Async<TimelineEvent> = Uninitialized,
|
||||
val messageBody: CharSequence = "",
|
||||
// For quick reactions
|
||||
val quickStates: Async<List<ToggleState>> = Uninitialized,
|
||||
// For actions
|
||||
val actions: List<EventSharedAction> = emptyList(),
|
||||
val expendedReportContentMenu: Boolean = false,
|
||||
val actionPermissions: ActionPermissions = ActionPermissions()
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||
|
||||
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
|
||||
|
||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
||||
|
||||
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
|
||||
|
||||
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
|
||||
}
|
|
@ -15,11 +15,8 @@
|
|||
*/
|
||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
|
@ -35,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
|
|||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
|
||||
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||
|
@ -47,46 +45,11 @@ import im.vector.riotx.core.platform.EmptyViewEvents
|
|||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.powerlevel.PowerLevelsObservableFactory
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import im.vector.riotx.features.html.VectorHtmlCompressor
|
||||
import im.vector.riotx.features.reactions.data.EmojiDataSource
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Quick reactions state
|
||||
*/
|
||||
data class ToggleState(
|
||||
val reaction: String,
|
||||
val isSelected: Boolean
|
||||
)
|
||||
|
||||
data class MessageActionState(
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val informationData: MessageInformationData,
|
||||
val timelineEvent: Async<TimelineEvent> = Uninitialized,
|
||||
val messageBody: CharSequence = "",
|
||||
// For quick reactions
|
||||
val quickStates: Async<List<ToggleState>> = Uninitialized,
|
||||
// For actions
|
||||
val actions: List<EventSharedAction> = emptyList(),
|
||||
val expendedReportContentMenu: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||
|
||||
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
|
||||
|
||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
||||
|
||||
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
|
||||
|
||||
fun canReact() = timelineEvent()?.canReact() == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Information related to an event and used to display preview in contextual bottom sheet.
|
||||
|
@ -121,6 +84,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
init {
|
||||
observeEvent()
|
||||
observeReactions()
|
||||
observePowerLevel()
|
||||
observeTimelineEventState()
|
||||
}
|
||||
|
||||
|
@ -138,6 +102,23 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
}
|
||||
|
||||
private fun observePowerLevel() {
|
||||
if (room == null) {
|
||||
return
|
||||
}
|
||||
PowerLevelsObservableFactory(room).createObservable()
|
||||
.subscribe {
|
||||
val powerLevelsHelper = PowerLevelsHelper(it)
|
||||
val canReact = powerLevelsHelper.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 && (
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -140,7 +140,7 @@ class RoomProfileFragment @Inject constructor(
|
|||
|
||||
private fun setupRecyclerView() {
|
||||
roomProfileController.callback = this
|
||||
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true)
|
||||
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.riotx.features.roomprofile.members
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.args
|
||||
|
@ -46,6 +47,13 @@ class RoomMemberListFragment @Inject constructor(
|
|||
|
||||
override fun getMenuRes() = R.menu.menu_room_member_list
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val canInvite = withState(viewModel) {
|
||||
it.actionsPermissions.canInvite
|
||||
}
|
||||
menu.findItem(R.id.menu_room_member_list_add_member).isVisible = canInvite
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_room_member_list_add_member -> {
|
||||
|
@ -61,6 +69,9 @@ class RoomMemberListFragment @Inject constructor(
|
|||
roomMemberListController.callback = this
|
||||
setupToolbar(roomSettingsToolbar)
|
||||
recyclerView.configureWith(roomMemberListController, hasFixedSize = true)
|
||||
viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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" />
|
||||
|
65
vector/src/main/res/layout/dialog_edit_power_level.xml
Normal file
65
vector/src/main/res/layout/dialog_edit_power_level.xml
Normal 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>
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 l’application 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 l’envoyer</string>
|
||||
|
@ -1018,10 +1015,6 @@ Veuillez noter que cette action redémarrera l’application 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 d’accueil le permet.</string>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">S’do 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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue