1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Merge pull request #9065 from element-hq/feature/room_v12_support

Room v12 support
This commit is contained in:
ganfra
2025-08-04 20:20:15 +02:00
committed by GitHub
77 changed files with 857 additions and 701 deletions

View File

@@ -120,6 +120,7 @@
<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_owner">Owner</string>
<string name="power_level_admin">Admin</string>
<string name="power_level_moderator">Moderator</string>
<string name="power_level_default">Default</string>
@@ -685,6 +686,7 @@
<string name="room_participants_leave_prompt_title">Leave room</string>
<string name="room_participants_leave_prompt_msg">Are you sure you want to leave the room?</string>
<string name="room_participants_leave_private_warning">This room is not public. You will not be able to rejoin without an invite.</string>
<string name="room_participants_leave_last_admin">You\'re the only admin of this room. Leaving it will mean no one has control over it.</string>
<string name="room_participants_header_direct_chats">Direct Messages</string>
@@ -2383,6 +2385,7 @@
<string name="room_member_power_level_invites">Invites</string>
<string name="room_member_power_level_users">Users</string>
<string name="room_member_power_level_owner_in">Owner in %1$s</string>
<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>

View File

@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Optional
@@ -95,6 +96,10 @@ class FlowRoom(private val room: Room) {
}
}
fun liveRoomPowerLevels(): Flow<RoomPowerLevels> {
return room.stateService().getRoomPowerLevelsLive().asFlow()
}
fun liveReadMarker(): Flow<Optional<String>> {
return room.readService().getReadMarkerLive().asFlow()
}

View File

@@ -40,8 +40,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
import org.matrix.android.sdk.common.SessionTestParams
@@ -500,12 +500,12 @@ class SpaceHierarchyTest : InstrumentedTest {
room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!)
commonTestHelper.retryPeriodically {
val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!!
val roomPowerLevels = aliceSession.getRoom(bobRoomId)!!
.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
?.let { RoomPowerLevels(it) }
roomPowerLevels!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
}
aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))

View File

@@ -30,15 +30,21 @@ object MatrixPatterns {
// Note: TLD is not mandatory (localhost, IP address...)
private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?"
private const val BASE_64_ALPHABET = "[0-9A-Za-z/\\+=]+"
private const val BASE_64_URL_SAFE_ALPHABET = "[0-9A-Za-z/\\-_]+"
// regex pattern to find matrix user ids in a string.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room ids in a string.
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "^!.+$DOMAIN_REGEX$"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
private const val MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX = "!$BASE_64_URL_SAFE_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS = MATRIX_ROOM_IDENTIFIER_DOMAINLESS_REGEX.toRegex()
// regex pattern to find room aliases in a string.
private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE)
@@ -48,11 +54,11 @@ object MatrixPatterns {
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find message ids in a string.
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+"
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$$BASE_64_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE)
// Ref: https://matrix.org/docs/spec/rooms/v4#event-ids
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+"
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$$BASE_64_URL_SAFE_ALPHABET"
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find group ids in a string.
@@ -76,7 +82,10 @@ object MatrixPatterns {
PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_ALIAS,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS,
PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER,
PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3,
PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4,
PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
)
@@ -97,7 +106,9 @@ object MatrixPatterns {
* @return true if the string is a valid room Id
*/
fun isRoomId(str: String?): Boolean {
return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER
return str != null &&
(str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER ||
str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER_DOMAINLESS)
}
/**

View File

@@ -16,8 +16,7 @@
package org.matrix.android.sdk.api.session.pushrules
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
class SenderNotificationPermissionCondition(
/**
@@ -35,8 +34,7 @@ class SenderNotificationPermissionCondition(
override fun technicalDescription() = "User power level <$key>"
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevels.notificationLevel(key)
fun isSatisfied(event: Event, roomPowerLevels: RoomPowerLevels): Boolean {
return event.senderId != null && roomPowerLevels.isUserAbleToTriggerNotification(event.senderId, key)
}
}

View File

@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room
import org.matrix.android.sdk.api.query.QueryStateEventValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
@@ -34,3 +35,10 @@ fun Room.getTimelineEvent(eventId: String): TimelineEvent? =
*/
fun Room.getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? =
stateService().getStateEvent(eventType, stateKey)
/**
* Get the current RoomPowerLevels of the room.
*/
fun Room.getRoomPowerLevels(): RoomPowerLevels {
return stateService().getRoomPowerLevels()
}

View File

@@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent.Companion.NOTIFICATIONS_ROOM_KEY
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
/**
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
@@ -34,7 +35,7 @@ data class PowerLevelsContent(
*/
@Json(name = "kick") val kick: Int? = null,
/**
* The level required to invite a user. Defaults to 50 if unspecified.
* The level required to invite a user. Defaults to 0 if unspecified.
*/
@Json(name = "invite") val invite: Int? = null,
/**
@@ -88,7 +89,7 @@ data class PowerLevelsContent(
* Get the notification level for a dedicated key.
*
* @param key the notification key
* @return the level, default to Moderator if the key is not found
* @return the level
*/
fun notificationLevel(key: String): Int {
return when (val value = notifications.orEmpty()[key]) {
@@ -96,10 +97,9 @@ data class PowerLevelsContent(
is String -> value.toInt()
is Double -> value.toInt()
is Int -> value
else -> Role.Moderator.value
else -> defaultNotificationLevel(key)
}
}
companion object {
/**
* Key to use for content.notifications and get the level required to trigger an @room notification. Defaults to 50 if unspecified.
@@ -108,11 +108,20 @@ data class PowerLevelsContent(
}
}
private fun defaultNotificationLevel(key: String): Int {
return when (key) {
NOTIFICATIONS_ROOM_KEY -> UserPowerLevel.Moderator.value
else -> UserPowerLevel.User.value
}
}
// Fallback to default value, defined in the Matrix specification
fun PowerLevelsContent.banOrDefault() = ban ?: Role.Moderator.value
fun PowerLevelsContent.kickOrDefault() = kick ?: Role.Moderator.value
fun PowerLevelsContent.inviteOrDefault() = invite ?: Role.Moderator.value
fun PowerLevelsContent.redactOrDefault() = redact ?: Role.Moderator.value
fun PowerLevelsContent.eventsDefaultOrDefault() = eventsDefault ?: Role.Default.value
fun PowerLevelsContent.usersDefaultOrDefault() = usersDefault ?: Role.Default.value
fun PowerLevelsContent.stateDefaultOrDefault() = stateDefault ?: Role.Moderator.value
fun PowerLevelsContent?.banOrDefault() = this?.ban ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.kickOrDefault() = this?.kick ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.inviteOrDefault() = this?.invite ?: UserPowerLevel.User.value
fun PowerLevelsContent?.redactOrDefault() = this?.redact ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.eventsDefaultOrDefault() = this?.eventsDefault ?: UserPowerLevel.User.value
fun PowerLevelsContent?.usersDefaultOrDefault() = this?.usersDefault ?: UserPowerLevel.User.value
fun PowerLevelsContent?.stateDefaultOrDefault() = this?.stateDefault ?: UserPowerLevel.Moderator.value
fun PowerLevelsContent?.notificationLevelOrDefault(key: String) = this?.notificationLevel(key) ?: defaultNotificationLevel(key)

View File

@@ -18,15 +18,39 @@ package org.matrix.android.sdk.api.session.room.model.create
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
/**
* Content of a m.room.create type event.
*/
@JsonClass(generateAdapter = true)
data class RoomCreateContent(
// Creator should be replaced by the sender of the event
@Json(name = "creator") val creator: String? = null,
@Json(name = "room_version") val roomVersion: String? = null,
@Json(name = "predecessor") val predecessor: Predecessor? = null,
// Defines the room type, see #RoomType (user extensible)
@Json(name = "type") val type: String? = null
@Json(name = "type") val type: String? = null,
@Json(name = "additional_creators") val additionalCreators: List<String>? = null,
)
data class RoomCreateContentWithSender(
val senderId: String,
val inner: RoomCreateContent
) {
val creators = setOf(senderId) + inner.additionalCreators.orEmpty().toSet()
}
fun Event.getRoomCreateContentWithSender(): RoomCreateContentWithSender? {
if (this.type != EventType.STATE_ROOM_CREATE) return null
val innerContent = getClearContent().toModel<RoomCreateContent>() ?: return null
val senderId = senderId ?: return null
return RoomCreateContentWithSender(senderId, innerContent)
}
fun RoomCreateContent.explicitlyPrivilegeRoomCreators(): Boolean {
val supportedRoomVersions = listOf("org.matrix.hydra.11", "12")
return supportedRoomVersions.contains(roomVersion)
}

View File

@@ -17,26 +17,21 @@
package org.matrix.android.sdk.api.session.room.powerlevels
sealed class Role(open val value: Int) : Comparable<Role> {
object Admin : Role(100)
object Moderator : Role(50)
object Default : Role(0)
data class Custom(override val value: Int) : Role(value)
override fun compareTo(other: Role): Int {
return value.compareTo(other.value)
}
enum class Role {
Creator,
SuperAdmin,
Admin,
Moderator,
User;
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)
fun getSuggestedRole(userPowerLevel: UserPowerLevel): Role {
return when {
userPowerLevel == UserPowerLevel.Infinite -> Creator
userPowerLevel >= UserPowerLevel.SuperAdmin -> SuperAdmin
userPowerLevel >= UserPowerLevel.Admin -> Admin
userPowerLevel >= UserPowerLevel.Moderator -> Moderator
else -> User
}
}
}

View File

@@ -19,17 +19,23 @@ package org.matrix.android.sdk.api.session.room.powerlevels
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.banOrDefault
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContentWithSender
import org.matrix.android.sdk.api.session.room.model.create.explicitlyPrivilegeRoomCreators
import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault
import org.matrix.android.sdk.api.session.room.model.inviteOrDefault
import org.matrix.android.sdk.api.session.room.model.kickOrDefault
import org.matrix.android.sdk.api.session.room.model.notificationLevelOrDefault
import org.matrix.android.sdk.api.session.room.model.redactOrDefault
import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
/**
* This class is an helper around PowerLevelsContent.
* This class is an helper around PowerLevelsContent and RoomCreateContent.
*/
class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
class RoomPowerLevels(
val powerLevelsContent: PowerLevelsContent?,
private val roomCreateContent: RoomCreateContentWithSender?,
) {
/**
* Returns the user power level of a dedicated user Id.
@@ -37,10 +43,14 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return the power level
*/
fun getUserPowerLevelValue(userId: String): Int {
return powerLevelsContent.users
fun getUserPowerLevel(userId: String): UserPowerLevel {
if (shouldGiveInfinitePowerLevel(userId)) return UserPowerLevel.Infinite
if (powerLevelsContent == null) return UserPowerLevel.User
val value = powerLevelsContent.users
?.get(userId)
?: powerLevelsContent.usersDefaultOrDefault()
return UserPowerLevel.Value(value)
}
/**
@@ -49,10 +59,9 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @param userId the user id
* @return the power level
*/
fun getUserRole(userId: String): Role {
val value = getUserPowerLevelValue(userId)
// I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web
return Role.fromValue(value, powerLevelsContent.eventsDefaultOrDefault())
fun getSuggestedRole(userId: String): Role {
val value = getUserPowerLevel(userId)
return Role.getSuggestedRole(value)
}
/**
@@ -65,14 +74,14 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
*/
fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean {
return if (userId.isNotEmpty()) {
val powerLevel = getUserPowerLevelValue(userId)
val minimumPowerLevel = powerLevelsContent.events?.get(eventType)
val powerLevel = getUserPowerLevel(userId)
val minimumPowerLevel = powerLevelsContent?.events?.get(eventType)
?: if (isState) {
powerLevelsContent.stateDefaultOrDefault()
} else {
powerLevelsContent.eventsDefaultOrDefault()
}
powerLevel >= minimumPowerLevel
powerLevel >= UserPowerLevel.Value(minimumPowerLevel)
} else false
}
@@ -82,8 +91,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to invite
*/
fun isUserAbleToInvite(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.inviteOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.inviteOrDefault())
}
/**
@@ -92,8 +101,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to ban
*/
fun isUserAbleToBan(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.banOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.banOrDefault())
}
/**
@@ -102,8 +111,8 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to kick
*/
fun isUserAbleToKick(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.kickOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.kickOrDefault())
}
/**
@@ -112,7 +121,22 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
* @return true if able to redact
*/
fun isUserAbleToRedact(userId: String): Boolean {
val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redactOrDefault()
val powerLevel = getUserPowerLevel(userId)
return powerLevel >= UserPowerLevel.Value(powerLevelsContent.redactOrDefault())
}
fun isUserAbleToTriggerNotification(userId: String, notificationKey: String): Boolean {
val userPowerLevel = getUserPowerLevel(userId)
val notificationPowerLevel = UserPowerLevel.Value(powerLevelsContent.notificationLevelOrDefault(key = notificationKey))
return userPowerLevel >= notificationPowerLevel
}
private fun shouldGiveInfinitePowerLevel(userId: String): Boolean {
if (roomCreateContent == null) return false
return if (roomCreateContent.inner.explicitlyPrivilegeRoomCreators()) {
roomCreateContent.creators.contains(userId)
} else {
false
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.powerlevels
sealed interface UserPowerLevel : Comparable<UserPowerLevel> {
data object Infinite : UserPowerLevel
@JvmInline
value class Value(val value: Int) : UserPowerLevel
override fun compareTo(other: UserPowerLevel): Int {
return when (this) {
Infinite -> when (other) {
Infinite -> 0
is Value -> 1
}
is Value -> when (other) {
Infinite -> -1
is Value -> value.compareTo(other.value)
}
}
}
companion object {
val User = Value(0)
val Moderator = Value(50)
val Admin = Value(100)
val SuperAdmin = Value(150)
}
}

View File

@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
@@ -106,4 +107,6 @@ interface StateService {
suspend fun setJoinRulePublic()
suspend fun setJoinRuleInviteOnly()
suspend fun setJoinRuleRestricted(allowList: List<String>)
fun getRoomPowerLevels(): RoomPowerLevels
fun getRoomPowerLevelsLive(): LiveData<RoomPowerLevels>
}

View File

@@ -17,15 +17,11 @@
package org.matrix.android.sdk.internal.session.permalinks
import org.matrix.android.sdk.api.MatrixPatterns.getServerName
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import java.net.URLEncoder
import javax.inject.Inject
@@ -101,10 +97,7 @@ internal class ViaParameterFinder @Inject constructor(
}
fun userCanInvite(userId: String, roomId: String): Boolean {
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
return powerLevelsHelper?.isUserAbleToInvite(userId) ?: false
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId)
return roomPowerLevels.isUserAbleToInvite(userId)
}
}

View File

@@ -15,24 +15,20 @@
*/
package org.matrix.android.sdk.internal.session.pushers
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.pushrules.ConditionResolver
import org.matrix.android.sdk.api.session.pushrules.ContainsDisplayNameCondition
import org.matrix.android.sdk.api.session.pushrules.EventMatchCondition
import org.matrix.android.sdk.api.session.pushrules.RoomMemberCountCondition
import org.matrix.android.sdk.api.session.pushrules.SenderNotificationPermissionCondition
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.getRoomPowerLevels
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import javax.inject.Inject
internal class DefaultConditionResolver @Inject constructor(
private val roomGetter: RoomGetter,
@UserId private val userId: String
@UserId private val userId: String,
) : ConditionResolver {
override fun resolveEventMatchCondition(
@@ -55,13 +51,8 @@ internal class DefaultConditionResolver @Inject constructor(
): Boolean {
val roomId = event.roomId ?: return false
val room = roomGetter.getRoom(roomId) ?: return false
val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
?: PowerLevelsContent()
return condition.isSatisfied(event, powerLevelsContent)
val roomPowerLevels = room.getRoomPowerLevels()
return condition.isSatisfied(event, roomPowerLevels)
}
override fun resolveContainsDisplayNameCondition(

View File

@@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.session.room
import io.realm.Realm
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
import org.matrix.android.sdk.api.session.events.model.Event
@@ -27,7 +26,6 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
@@ -36,7 +34,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
@@ -62,6 +59,7 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
@@ -216,9 +214,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
in EventType.POLL_END.values -> {
sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
getPowerLevelsHelper(event.roomId)?.let {
pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
}
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(event.roomId)
pollAggregationProcessor.handlePollEndEvent(session, roomPowerLevels, realm, event)
}
}
in EventType.STATE_ROOM_BEACON_INFO.values -> {
@@ -381,12 +378,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
}
}
private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? {
return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
}
private fun handleInitialAggregatedRelations(
realm: Realm,
event: Event,

View File

@@ -32,7 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo
import org.matrix.android.sdk.api.session.room.model.VoteSummary
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@@ -160,13 +160,13 @@ internal class DefaultPollAggregationProcessor @Inject constructor(
return true
}
override fun handlePollEndEvent(session: Session, powerLevelsHelper: PowerLevelsHelper, realm: Realm, event: Event): Boolean {
override fun handlePollEndEvent(session: Session, roomPowerLevels: RoomPowerLevels, realm: Realm, event: Event): Boolean {
val roomId = event.roomId ?: return false
val pollEventId = event.getRelationContent()?.eventId ?: return false
val pollOwnerId = getPollEvent(session, roomId, pollEventId)?.root?.senderId
val isPollOwner = pollOwnerId == event.senderId
if (!isPollOwner && !powerLevelsHelper.isUserAbleToRedact(event.senderId ?: "")) {
if (!isPollOwner && !roomPowerLevels.isUserAbleToRedact(event.senderId ?: "")) {
return false
}

View File

@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll
import io.realm.Realm
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
internal interface PollAggregationProcessor {
/**
@@ -48,7 +48,7 @@ internal interface PollAggregationProcessor {
*/
fun handlePollEndEvent(
session: Session,
powerLevelsHelper: PowerLevelsHelper,
roomPowerLevels: RoomPowerLevels,
realm: Realm,
event: Event
): Boolean

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.powerlevels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.getRoomCreateContentWithSender
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
internal fun StateEventDataSource.getRoomPowerLevels(roomId: String): RoomPowerLevels {
val powerLevelsEvent = getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
val roomCreateEvent = getStateEvent(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty)
return createRoomPowerLevels(powerLevelsEvent = powerLevelsEvent, roomCreateEvent = roomCreateEvent)
}
internal fun StateEventDataSource.getRoomPowerLevelsLive(roomId: String): LiveData<RoomPowerLevels> {
val powerLevelsEventLive = getStateEventLive(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
val roomCreateEventLive = getStateEventLive(roomId, EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty)
val resultLiveData = MediatorLiveData<RoomPowerLevels>()
fun emitIfReady(powerLevelEvent: Optional<Event>?, roomCreateEvent: Optional<Event>?) {
if (powerLevelEvent != null && roomCreateEvent != null) {
val roomPowerLevels = createRoomPowerLevels(powerLevelEvent.get(), roomCreateEvent.get())
resultLiveData.postValue(roomPowerLevels)
}
}
resultLiveData.apply {
var powerLevelEvent: Optional<Event>? = null
var roomCreateEvent: Optional<Event>? = null
addSource(powerLevelsEventLive) {
powerLevelEvent = it
emitIfReady(powerLevelEvent, roomCreateEvent)
}
addSource(roomCreateEventLive) {
roomCreateEvent = it
emitIfReady(powerLevelEvent, roomCreateEvent)
}
}
return resultLiveData
}
private fun createRoomPowerLevels(powerLevelsEvent: Event?, roomCreateEvent: Event?): RoomPowerLevels {
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
val roomCreateContent = roomCreateEvent?.getRoomCreateContentWithSender()
return RoomPowerLevels(powerLevelsContent, roomCreateContent)
}

View File

@@ -31,11 +31,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevelsLive
internal class DefaultStateService @AssistedInject constructor(
@Assisted private val roomId: String,
@@ -65,6 +68,14 @@ internal class DefaultStateService @AssistedInject constructor(
return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
}
override fun getRoomPowerLevels(): RoomPowerLevels {
return stateEventDataSource.getRoomPowerLevels(roomId)
}
override fun getRoomPowerLevelsLive(): LiveData<RoomPowerLevels> {
return stateEventDataSource.getRoomPowerLevelsLive(roomId)
}
override suspend fun sendStateEvent(
eventType: String,
stateKey: String,

View File

@@ -34,7 +34,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContentWithSender
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
@@ -313,13 +314,25 @@ internal class RoomSummaryUpdater @Inject constructor(
// check if sender can post child relation in parent?
val senderId = parentInfo.stateEventSender
val parentRoomId = parentInfo.roomId
val powerLevelsHelper = CurrentStateEventEntity
val powerLevelsContent = CurrentStateEventEntity
.getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_POWER_LEVELS)
?.root
?.let { ContentMapper.map(it.content).toModel<PowerLevelsContent>() }
?.let { PowerLevelsHelper(it) }
isValidRelation = powerLevelsHelper?.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD) ?: false
val roomCreateContent = CurrentStateEventEntity
.getOrNull(realm, parentRoomId, "", EventType.STATE_ROOM_CREATE)
?.root
?.let {
val content = ContentMapper.map(it.content).toModel<RoomCreateContent>()
val sender = it.sender
if (content != null && sender != null) {
RoomCreateContentWithSender(sender, content)
} else {
null
}
}
val roomPowerLevels = RoomPowerLevels(powerLevelsContent, roomCreateContent)
isValidRelation = roomPowerLevels.isUserAllowedToSend(senderId, true, EventType.STATE_SPACE_CHILD)
}
if (isValidRelation) {

View File

@@ -23,11 +23,10 @@ import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.version.RoomVersionService
import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
internal class DefaultRoomVersionService @AssistedInject constructor(
@@ -71,11 +70,8 @@ internal class DefaultRoomVersionService @AssistedInject constructor(
}
override fun userMayUpgradeRoom(userId: String): Boolean {
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
return powerLevelsHelper?.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE) ?: false
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId)
return roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_TOMBSTONE)
}
companion object {

View File

@@ -35,8 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.api.session.space.JoinSpaceResult
import org.matrix.android.sdk.api.session.space.Space
@@ -47,11 +46,13 @@ import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.api.session.space.model.SpaceChildSummaryEvent
import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.SpaceGetter
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
@@ -83,7 +84,7 @@ internal class DefaultSpaceService @Inject constructor(
if (isPublic) {
this.roomAliasName = roomAliasLocalPart
this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
invite = if (isPublic) Role.Default.value else Role.Moderator.value
invite = UserPowerLevel.User.value
)
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
@@ -253,15 +254,8 @@ internal class DefaultSpaceService @Inject constructor(
if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) {
throw UnsupportedOperationException("Cannot add canonical child if not member of parent")
}
val powerLevelsEvent = stateEventDataSource.getStateEvent(
roomId = parentSpaceId,
eventType = EventType.STATE_ROOM_POWER_LEVELS,
stateKey = QueryStringValue.IsEmpty
)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
?: throw UnsupportedOperationException("Cannot add canonical child, missing power level")
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) {
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(parentSpaceId)
if (!roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) {
throw UnsupportedOperationException("Cannot add canonical child, not enough power level")
}
}

View File

@@ -30,15 +30,13 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
import org.matrix.android.sdk.internal.session.room.powerlevels.getRoomPowerLevels
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory
@@ -200,12 +198,7 @@ internal class WidgetManager @Inject constructor(
}
fun hasPermissionsToHandleWidgets(roomId: String): Boolean {
val powerLevelsEvent = stateEventDataSource.getStateEvent(
roomId = roomId,
eventType = EventType.STATE_ROOM_POWER_LEVELS,
stateKey = QueryStringValue.IsEmpty
)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
val roomPowerLevels = stateEventDataSource.getRoomPowerLevels(roomId)
return roomPowerLevels.isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
}
}

View File

@@ -33,7 +33,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getTimelineEvent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
@@ -255,9 +255,9 @@ class DefaultPollAggregationProcessorTest {
every { room.getTimelineEvent(eventId) } returns if (hasExistingTimelineEvent) A_TIMELINE_EVENT else null
}
private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): PowerLevelsHelper {
val powerLevelsHelper = mockk<PowerLevelsHelper>()
every { powerLevelsHelper.isUserAbleToRedact(userId) } returns isAbleToRedact
return powerLevelsHelper
private fun mockRedactionPowerLevels(userId: String, isAbleToRedact: Boolean): RoomPowerLevels {
val roomPowerLevels = mockk<RoomPowerLevels>()
every { roomPowerLevels.isUserAbleToRedact(userId) } returns isAbleToRedact
return roomPowerLevels
}
}

View File

@@ -51,7 +51,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_EMAIL
@@ -372,13 +372,13 @@ internal class DefaultCreateLocalRoomStateEventsTaskTest {
// Power levels
val powerLevelsContent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }?.content.toModel<PowerLevelsContent>()
powerLevelsContent.shouldNotBeNull()
powerLevelsContent.ban shouldBeEqualTo Role.Moderator.value
powerLevelsContent.kick shouldBeEqualTo Role.Moderator.value
powerLevelsContent.invite shouldBeEqualTo Role.Moderator.value
powerLevelsContent.redact shouldBeEqualTo Role.Moderator.value
powerLevelsContent.eventsDefault shouldBeEqualTo Role.Default.value
powerLevelsContent.usersDefault shouldBeEqualTo Role.Default.value
powerLevelsContent.stateDefault shouldBeEqualTo Role.Moderator.value
powerLevelsContent.ban shouldBeEqualTo UserPowerLevel.Moderator.value
powerLevelsContent.kick shouldBeEqualTo UserPowerLevel.Moderator.value
powerLevelsContent.invite shouldBeEqualTo UserPowerLevel.User.value
powerLevelsContent.redact shouldBeEqualTo UserPowerLevel.Moderator.value
powerLevelsContent.eventsDefault shouldBeEqualTo UserPowerLevel.User.value
powerLevelsContent.usersDefault shouldBeEqualTo UserPowerLevel.User.value
powerLevelsContent.stateDefault shouldBeEqualTo UserPowerLevel.Moderator.value
// Guest access
result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
?.content.toModel<RoomGuestAccessContent>()?.guestAccess shouldBeEqualTo GuestAccess.Forbidden

View File

@@ -10,12 +10,27 @@ package im.vector.app.core.utils
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import dagger.Binds
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
class PermissionChecker @Inject constructor(
interface PermissionChecker {
@InstallIn(SingletonComponent::class)
@dagger.Module
interface Module {
@Binds
fun bindPermissionChecker(permissionChecker: AndroidPermissionChecker): PermissionChecker
}
fun checkPermission(vararg permissions: String): Boolean
}
class AndroidPermissionChecker @Inject constructor(
private val applicationContext: Context,
) {
fun checkPermission(vararg permissions: String): Boolean {
) : PermissionChecker {
override fun checkPermission(vararg permissions: String): Boolean {
return permissions.any { permission ->
ActivityCompat.checkSelfPermission(applicationContext, permission) != PackageManager.PERMISSION_GRANTED
}

View File

@@ -54,7 +54,6 @@ import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.raw.wellknown.CryptoConfig
import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault
import im.vector.app.features.raw.wellknown.withElementWellKnown
@@ -110,7 +109,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachme
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.read.ReadService
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -303,12 +301,12 @@ class TimelineViewModel @AssistedInject constructor(
private fun observePowerLevel() {
if (room == null) return
PowerLevelsFlowFactory(room).createFlow()
.onEach {
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
room.flow().liveRoomPowerLevels()
.onEach { powerLevels ->
val canInvite = powerLevels.isUserAbleToInvite(session.myUserId)
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
val isAllowedToStartWebRTCCall = powerLevels.isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
val isAllowedToSetupEncryption = powerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
setState {
copy(
canInvite = canInvite,

View File

@@ -30,7 +30,6 @@ import im.vector.app.features.home.room.detail.ChatEffect
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.toMessageType
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.VoiceFailure
@@ -69,7 +68,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
@@ -180,10 +178,10 @@ class MessageComposerViewModel @AssistedInject constructor(
private fun observePowerLevelAndEncryption(room: Room) {
combine(
PowerLevelsFlowFactory(room).createFlow(),
room.flow().liveRoomPowerLevels(),
room.flow().liveRoomSummary().unwrap()
) { pl, sum ->
val canSendMessage = PowerLevelsHelper(pl).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
val canSendMessage = pl.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
if (canSendMessage) {
val isE2E = sum.isEncrypted
if (isE2E) {

View File

@@ -23,7 +23,6 @@ import im.vector.app.features.home.room.detail.timeline.format.NoticeEventFormat
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.strings.CommonStrings
@@ -49,7 +48,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
@@ -116,12 +114,11 @@ class MessageActionsViewModel @AssistedInject constructor(
if (room == null) {
return
}
PowerLevelsFlowFactory(room).createFlow()
.onEach {
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)
room.flow().liveRoomPowerLevels()
.onEach { roomPowerLevels ->
val canReact = roomPowerLevels.isUserAllowedToSend(session.myUserId, false, EventType.REACTION)
val canRedact = roomPowerLevels.isUserAbleToRedact(session.myUserId)
val canSendMessage = roomPowerLevels.isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
val permissions = ActionPermissions(canSendMessage = canSendMessage, canRedact = canRedact, canReact = canReact)
setState {
copy(actionPermissions = permissions)

View File

@@ -24,16 +24,13 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
import im.vector.lib.strings.CommonPlurals
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.getRoomPowerLevels
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
@@ -303,9 +300,7 @@ class MergedHeaderItemFactory @Inject constructor(
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val powerLevelsHelper = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId)
?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)?.content?.toModel<PowerLevelsContent>() }
?.let { PowerLevelsHelper(it) }
val roomPowerLevels = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId)?.getRoomPowerLevels()
val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
val attributes = MergedRoomCreationItem.Attributes(
isCollapsed = isCollapsed,
@@ -320,10 +315,10 @@ class MergedHeaderItemFactory @Inject constructor(
callback = callback,
currentUserId = currentUserId,
roomSummary = partialState.roomSummary,
canInvite = powerLevelsHelper?.isUserAbleToInvite(currentUserId) ?: false,
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
canInvite = roomPowerLevels?.isUserAbleToInvite(currentUserId) ?: false,
canChangeAvatar = roomPowerLevels?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
canChangeTopic = roomPowerLevels?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
canChangeName = roomPowerLevels?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
)
MergedRoomCreationItem_()
.id(mergeId)

View File

@@ -41,7 +41,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
import timber.log.Timber
@@ -122,8 +122,8 @@ class NoticeEventFormatter @Inject constructor(
userIds.addAll(previousPowerLevelsContent.users.orEmpty().keys)
val diffs = ArrayList<String>()
userIds.forEach { userId ->
val from = PowerLevelsHelper(previousPowerLevelsContent).getUserRole(userId)
val to = PowerLevelsHelper(powerLevelsContent).getUserRole(userId)
val from = RoomPowerLevels(previousPowerLevelsContent, null).getSuggestedRole(userId)
val to = RoomPowerLevels(powerLevelsContent, null).getSuggestedRole(userId)
if (from != to) {
val fromStr = roleFormatter.format(from)
val toStr = roleFormatter.format(to)

View File

@@ -25,7 +25,6 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
@@ -45,6 +44,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
@@ -422,7 +422,7 @@ class RoomListFragment :
}
}
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
private suspend fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
@@ -451,26 +451,11 @@ class RoomListFragment :
}
}
private fun promptLeaveRoom(roomId: String) {
val isPublicRoom = roomListViewModel.isPublicRoom(roomId)
val message = buildString {
append(getString(CommonStrings.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(CommonStrings.room_participants_leave_private_warning))
}
private suspend fun promptLeaveRoom(roomId: String) {
val warning = roomListViewModel.getLeaveRoomWarning(roomId)
LeaveRoomPrompt.show(requireContext(), warning) {
roomListViewModel.handle(RoomListAction.LeaveRoom(roomId))
}
MaterialAlertDialogBuilder(
requireContext(),
if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
roomListViewModel.handle(RoomListAction.LeaveRoom(roomId))
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
override fun invalidate() = withState(roomListViewModel) { state ->

View File

@@ -26,6 +26,8 @@ import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.analytics.plan.JoinedRoom
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.app.features.room.getLeaveRoomWarning
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -41,7 +43,6 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
@@ -150,8 +151,8 @@ class RoomListViewModel @AssistedInject constructor(
}
}
fun isPublicRoom(roomId: String): Boolean {
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
suspend fun getLeaveRoomWarning(roomId: String): LeaveRoomPrompt.Warning {
return session.getLeaveRoomWarning(roomId)
}
// PRIVATE METHODS *****************************************************************************

View File

@@ -18,7 +18,6 @@ import androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.fragmentViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
@@ -36,7 +35,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import im.vector.app.features.home.room.list.home.header.HomeRoomsHeadersController
import im.vector.app.features.home.room.list.home.invites.InvitesActivity
import im.vector.lib.strings.CommonStrings
import im.vector.app.features.room.LeaveRoomPrompt
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -103,7 +102,7 @@ class HomeRoomListFragment :
}
}
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
private suspend fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
@@ -185,26 +184,11 @@ class HomeRoomListFragment :
concatAdapter.addAdapter(roomsAdapter)
}
private fun promptLeaveRoom(roomId: String) {
val isPublicRoom = roomListViewModel.isPublicRoom(roomId)
val message = buildString {
append(getString(CommonStrings.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(CommonStrings.room_participants_leave_private_warning))
}
private suspend fun promptLeaveRoom(roomId: String) {
val warning = roomListViewModel.getLeaveRoomWarning(roomId)
LeaveRoomPrompt.show(requireContext(), warning) {
roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId))
}
MaterialAlertDialogBuilder(
requireContext(),
if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId))
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
private fun onInvitesCounterClicked() {

View File

@@ -26,6 +26,8 @@ import im.vector.app.features.analytics.extensions.toTrackingValue
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.app.features.room.getLeaveRoomWarning
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -53,7 +55,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toOption
@@ -331,8 +332,8 @@ class HomeRoomListViewModel @AssistedInject constructor(
filteredPagedRoomSummariesLive.queryParams = getFilteredQueryParams(newFilter, filteredPagedRoomSummariesLive.queryParams)
}
fun isPublicRoom(roomId: String): Boolean {
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
suspend fun getLeaveRoomWarning(roomId: String): LeaveRoomPrompt.Warning {
return session.getLeaveRoomWarning(roomId)
}
private fun handleSelectRoom(action: HomeRoomListAction.SelectRoom) = withState {

View File

@@ -19,7 +19,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.PermissionChecker
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.location.domain.usecase.CompareLocationsUseCase
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -34,8 +33,8 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
/**
@@ -73,13 +72,12 @@ class LocationSharingViewModel @AssistedInject constructor(
}
private fun observePowerLevelsForLiveLocationSharing() {
PowerLevelsFlowFactory(room).createFlow()
room.flow().liveRoomPowerLevels()
.distinctUntilChanged()
.setOnEach {
val powerLevelsHelper = PowerLevelsHelper(it)
.setOnEach { roomPowerLevels ->
val canShareLiveLocation = EventType.STATE_ROOM_BEACON_INFO.values
.all { beaconInfoType ->
powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, beaconInfoType)
roomPowerLevels.isUserAllowedToSend(session.myUserId, true, beaconInfoType)
}
copy(canShareLiveLocation = canShareLiveLocation)

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2020-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.features.powerlevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.mapOptional
import org.matrix.android.sdk.flow.unwrap
class PowerLevelsFlowFactory(private val room: Room) {
fun createFlow(): Flow<PowerLevelsContent> {
return room.flow()
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.flowOn(Dispatchers.Default)
.unwrap()
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.features.powerlevel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.flow.flow
fun Role.isOwner() = this == Role.Creator || this == Role.SuperAdmin
fun Room.membersByRoleFlow(): Flow<Map<Role, List<RoomMemberSummary>>> {
val roomMembersFlow = flow().liveRoomMembers(roomMemberQueryParams())
val roomPowerLevelsFlow = flow().liveRoomPowerLevels()
return combine(roomMembersFlow, roomPowerLevelsFlow) { roomMembers, roomPowerLevels ->
roomMembers.groupBy { roomPowerLevels.getSuggestedRole(it.userId) }
}.distinctUntilChanged()
}
fun Room.isLastAdminFlow(userId: String): Flow<Boolean> {
return membersByRoleFlow().map { membersByRole ->
val creatorMembers = membersByRole[Role.Creator].orEmpty()
val superAdminMembers = membersByRole[Role.SuperAdmin].orEmpty()
val adminMembers = membersByRole[Role.Admin].orEmpty()
val joinedAdmins = (adminMembers + creatorMembers + superAdminMembers).filter { it.membership == Membership.JOIN }
if (joinedAdmins.size == 1) {
joinedAdmins.first().userId == userId
} else {
false
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.features.room
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.features.powerlevel.isLastAdminFlow
import im.vector.app.features.room.LeaveRoomPrompt.Warning
import im.vector.lib.strings.CommonStrings
import im.vector.lib.ui.styles.R
import kotlinx.coroutines.flow.first
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.state.isPublic
object LeaveRoomPrompt {
enum class Warning {
LAST_ADMIN,
PRIVATE_ROOM,
NONE
}
fun show(
context: Context,
warning: Warning,
onLeaveClick: () -> Unit
) {
val hasWarning = warning != Warning.NONE
val message = buildString {
append(context.getString(CommonStrings.room_participants_leave_prompt_msg))
if (hasWarning) append("\n\n")
when (warning) {
Warning.LAST_ADMIN -> append(context.getString(CommonStrings.room_participants_leave_last_admin))
Warning.PRIVATE_ROOM -> append(context.getString(CommonStrings.room_participants_leave_private_warning))
Warning.NONE -> Unit
}
}
MaterialAlertDialogBuilder(
context,
if (hasWarning) R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive else 0
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
onLeaveClick()
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
}
suspend fun Session.getLeaveRoomWarning(roomId: String): Warning {
val room = getRoom(roomId) ?: return Warning.NONE
val isLastAdmin = room.isLastAdminFlow(myUserId).first()
return when {
isLastAdmin -> Warning.LAST_ADMIN
!room.stateService().isPublic() -> Warning.PRIVATE_ROOM
else -> Warning.NONE
}
}

View File

@@ -8,6 +8,7 @@
package im.vector.app.features.roommemberprofile
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
sealed class RoomMemberProfileAction : VectorViewModelAction {
object RetryFetchingInfo : RoomMemberProfileAction()
@@ -18,7 +19,7 @@ sealed class RoomMemberProfileAction : VectorViewModelAction {
object InviteUser : RoomMemberProfileAction()
object VerifyUser : RoomMemberProfileAction()
object ShareRoomMemberProfile : RoomMemberProfileAction()
data class SetPowerLevel(val previousValue: Int, val newValue: Int, val askForValidation: Boolean) : RoomMemberProfileAction()
data class SetPowerLevel(val previousValue: UserPowerLevel, val newValue: UserPowerLevel.Value, val askForValidation: Boolean) : RoomMemberProfileAction()
data class SetUserColorOverride(val newColorSpec: String) : RoomMemberProfileAction()
data class OpenOrCreateDm(val userId: String) : RoomMemberProfileAction()
}

View File

@@ -17,8 +17,7 @@ import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import im.vector.lib.strings.CommonStrings
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import javax.inject.Inject
class RoomMemberProfileController @Inject constructor(
@@ -38,7 +37,7 @@ class RoomMemberProfileController @Inject constructor(
fun onOverrideColorClicked()
fun onJumpToReadReceiptClicked()
fun onMentionClicked()
fun onEditPowerLevel(currentRole: Role)
fun onEditPowerLevel(userPowerLevel: UserPowerLevel.Value)
fun onKickClicked(isSpace: Boolean)
fun onBanClicked(isSpace: Boolean, isUserBanned: Boolean)
fun onCancelInviteClicked()
@@ -243,14 +242,14 @@ 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)
val roomPowerLevels = state.roomPowerLevels ?: return
val userPowerLevel = roomPowerLevels.getUserPowerLevel(state.userId)
val myPowerLevel = roomPowerLevels.getUserPowerLevel(session.myUserId)
if ((!state.isMine && myPowerLevel <= userPowerLevel)) {
return
}
if (userPowerLevel !is UserPowerLevel.Value) return
val membership = state.asyncMembership() ?: return
val canKick = !state.isMine && state.actionPermissions.canKick
val canBan = !state.isMine && state.actionPermissions.canBan

View File

@@ -51,7 +51,7 @@ import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
import im.vector.lib.strings.CommonStrings
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject
@@ -377,9 +377,9 @@ class RoomMemberProfileFragment :
.show()
}
override fun onEditPowerLevel(currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), CommonStrings.power_level_edit_title, currentRole) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(currentRole.value, newPowerLevel, true))
override fun onEditPowerLevel(userPowerLevel: UserPowerLevel.Value) {
EditPowerLevelDialogs.showChoice(requireActivity(), CommonStrings.power_level_edit_title, userPowerLevel) { newPowerLevel ->
viewModel.handle(RoomMemberProfileAction.SetPowerLevel(userPowerLevel, newPowerLevel, true))
}
}

View File

@@ -8,6 +8,7 @@
package im.vector.app.features.roommemberprofile
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
/**
* Transient events for RoomMemberProfile.
@@ -22,8 +23,8 @@ sealed class RoomMemberProfileViewEvents : VectorViewEvents {
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 ShowPowerLevelValidation(val currentValue: UserPowerLevel, val newValue: UserPowerLevel.Value) : RoomMemberProfileViewEvents()
data class ShowPowerLevelDemoteWarning(val currentValue: UserPowerLevel, val newValue: UserPowerLevel.Value) : RoomMemberProfileViewEvents()
data class StartVerification(
val userId: String,

View File

@@ -23,7 +23,6 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
@@ -42,9 +41,9 @@ import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.toMatrixItem
@@ -233,15 +232,15 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
if (room == null || action.previousValue == action.newValue) {
return@withState
}
val currentPowerLevelsContent = state.powerLevelsContent ?: return@withState
val myPowerLevel = PowerLevelsHelper(currentPowerLevelsContent).getUserPowerLevelValue(session.myUserId)
val roomPowerLevels = state.roomPowerLevels ?: return@withState
val myPowerLevel = roomPowerLevels.getUserPowerLevel(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 {
val newPowerLevelsContent = currentPowerLevelsContent
.setUserPowerLevel(state.userId, action.newValue)
val newPowerLevelsContent = (roomPowerLevels.powerLevelsContent ?: PowerLevelsContent())
.setUserPowerLevel(state.userId, action.newValue.value)
.toContent()
viewModelScope.launch {
_viewEvents.post(RoomMemberProfileViewEvents.Loading())
@@ -361,19 +360,17 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
private fun observeRoomSummaryAndPowerLevels(room: Room) {
val roomSummaryLive = room.flow().liveRoomSummary().unwrap()
val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow()
powerLevelsContentLive
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
val powerLevelsFlow = room.flow().liveRoomPowerLevels()
powerLevelsFlow
.onEach { roomPowerLevels ->
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)
canKick = roomPowerLevels.isUserAbleToKick(session.myUserId),
canBan = roomPowerLevels.isUserAbleToBan(session.myUserId),
canInvite = roomPowerLevels.isUserAbleToInvite(session.myUserId),
canEditPowerLevel = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_POWER_LEVELS)
)
setState {
copy(powerLevelsContent = it, actionPermissions = permissions)
copy(roomPowerLevels = roomPowerLevels, actionPermissions = permissions)
}
}.launchIn(viewModelScope)
@@ -388,14 +385,14 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
copy(isRoomEncrypted = false)
}
}
roomSummaryLive.combine(powerLevelsContentLive) { roomSummary, powerLevelsContent ->
roomSummaryLive.combine(powerLevelsFlow) { roomSummary, roomPowerLevels ->
val roomName = roomSummary.toMatrixItem().getBestName()
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
when (val userPowerLevel = powerLevelsHelper.getUserRole(initialState.userId)) {
when (roomPowerLevels.getSuggestedRole(initialState.userId)) {
Role.SuperAdmin,
Role.Creator -> stringProvider.getString(CommonStrings.room_member_power_level_owner_in, roomName)
Role.Admin -> stringProvider.getString(CommonStrings.room_member_power_level_admin_in, roomName)
Role.Moderator -> stringProvider.getString(CommonStrings.room_member_power_level_moderator_in, roomName)
Role.Default -> stringProvider.getString(CommonStrings.room_member_power_level_default_in, roomName)
is Role.Custom -> stringProvider.getString(CommonStrings.room_member_power_level_custom_in, userPowerLevel.value, roomName)
Role.User -> stringProvider.getString(CommonStrings.room_member_power_level_default_in, roomName)
}
}.execute {
copy(userPowerLevelString = it)

View File

@@ -12,7 +12,7 @@ import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.api.util.MatrixItem
data class RoomMemberProfileViewState(
@@ -24,7 +24,7 @@ data class RoomMemberProfileViewState(
val isIgnored: Async<Boolean> = Uninitialized,
val isRoomEncrypted: Boolean = false,
val isAlgorithmSupported: Boolean = true,
val powerLevelsContent: PowerLevelsContent? = null,
val roomPowerLevels: RoomPowerLevels? = null,
val userPowerLevelString: Async<String> = Uninitialized,
val userMatrixItem: Async<MatrixItem> = Uninitialized,
val userMXCrossSigningInfo: MXCrossSigningInfo? = null,

View File

@@ -17,8 +17,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.databinding.DialogEditPowerLevelBinding
import im.vector.app.features.powerlevel.isOwner
import im.vector.lib.strings.CommonStrings
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
object EditPowerLevelDialogs {
@@ -26,46 +28,45 @@ object EditPowerLevelDialogs {
fun showChoice(
activity: Activity,
@StringRes titleRes: Int,
currentRole: Role,
listener: (Int) -> Unit
currentPowerLevel: UserPowerLevel.Value,
listener: (UserPowerLevel.Value) -> Unit
) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_edit_power_level, null)
val views = DialogEditPowerLevelBinding.bind(dialogLayout)
views.powerLevelRadioGroup.setOnCheckedChangeListener { _, checkedId ->
views.powerLevelCustomEditLayout.isVisible = checkedId == R.id.powerLevelCustomRadio
}
views.powerLevelCustomEdit.setText("${currentRole.value}")
val currentRole = Role.getSuggestedRole(currentPowerLevel)
when (currentRole) {
Role.Creator,
Role.SuperAdmin -> views.powerLevelOwnerRadio.isChecked = true
Role.Admin -> views.powerLevelAdminRadio.isChecked = true
Role.Moderator -> views.powerLevelModeratorRadio.isChecked = true
Role.Default -> views.powerLevelDefaultRadio.isChecked = true
else -> views.powerLevelCustomRadio.isChecked = true
Role.User -> views.powerLevelDefaultRadio.isChecked = true
}
views.powerLevelOwnerRadio.isVisible = currentRole.isOwner()
MaterialAlertDialogBuilder(activity)
.setTitle(titleRes)
.setView(dialogLayout)
.setPositiveButton(CommonStrings.edit) { _, _ ->
val newValue = when (views.powerLevelRadioGroup.checkedRadioButtonId) {
R.id.powerLevelAdminRadio -> Role.Admin.value
R.id.powerLevelModeratorRadio -> Role.Moderator.value
R.id.powerLevelDefaultRadio -> Role.Default.value
else -> {
views.powerLevelCustomEdit.text?.toString()?.toInt() ?: currentRole.value
}
R.id.powerLevelOwnerRadio -> UserPowerLevel.SuperAdmin
R.id.powerLevelAdminRadio -> UserPowerLevel.Admin
R.id.powerLevelModeratorRadio -> UserPowerLevel.Moderator
R.id.powerLevelDefaultRadio -> UserPowerLevel.User
else -> null
}
if (newValue != null) {
listener(newValue)
}
listener(newValue)
}
.setNegativeButton(CommonStrings.action_cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener
{ dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.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()
}

View File

@@ -45,6 +45,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.navigation.SettingsActivityPayload
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -320,25 +321,16 @@ class RoomProfileFragment :
}
override fun onLeaveRoomClicked() {
val isPublicRoom = roomProfileViewModel.isPublicRoom()
val message = buildString {
append(getString(CommonStrings.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(CommonStrings.room_participants_leave_private_warning))
withState(roomProfileViewModel) { state ->
val warning = when {
state.isLastAdmin -> LeaveRoomPrompt.Warning.LAST_ADMIN
state.roomSummary()?.isPublic == false -> LeaveRoomPrompt.Warning.PRIVATE_ROOM
else -> LeaveRoomPrompt.Warning.NONE
}
LeaveRoomPrompt.show(requireContext(), warning) {
roomProfileViewModel.handle(RoomProfileAction.LeaveRoom)
}
}
MaterialAlertDialogBuilder(
requireContext(),
if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
roomProfileViewModel.handle(RoomProfileAction.LeaveRoom)
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
override fun onRoomAliasesClicked() {

View File

@@ -19,7 +19,7 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.powerlevel.isLastAdminFlow
import im.vector.app.features.session.coroutineScope
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.Dispatchers
@@ -39,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.state.isPublic
import org.matrix.android.sdk.flow.FlowRoom
import org.matrix.android.sdk.flow.flow
@@ -72,6 +71,14 @@ class RoomProfileViewModel @AssistedInject constructor(
observePermissions()
observePowerLevels()
observeCryptoSettings(flowRoom)
observeIsLastAdmin()
}
private fun observeIsLastAdmin() {
room.isLastAdminFlow(session.myUserId)
.onEach { isLastAdmin ->
setState { copy(isLastAdmin = isLastAdmin) }
}.launchIn(viewModelScope)
}
private fun observeCryptoSettings(flowRoom: FlowRoom) {
@@ -113,11 +120,10 @@ class RoomProfileViewModel @AssistedInject constructor(
}
private fun observePowerLevels() {
val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow()
val powerLevelsContentLive = room.flow().liveRoomPowerLevels()
powerLevelsContentLive
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
val canUpdateRoomState = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
.onEach { roomPowerLevels ->
val canUpdateRoomState = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
setState {
copy(canUpdateRoomState = canUpdateRoomState)
}
@@ -156,12 +162,10 @@ class RoomProfileViewModel @AssistedInject constructor(
}
private fun observePermissions() {
PowerLevelsFlowFactory(room)
.createFlow()
.setOnEach {
val powerLevelsHelper = PowerLevelsHelper(it)
room.flow().liveRoomPowerLevels()
.setOnEach { roomPowerLevels ->
val permissions = RoomProfileViewState.ActionPermissions(
canEnableEncryption = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
canEnableEncryption = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
)
copy(actionPermissions = permissions)
}

View File

@@ -30,6 +30,7 @@ data class RoomProfileViewState(
val encryptToVerifiedDeviceOnly: Async<Boolean> = Uninitialized,
val globalCryptoConfig: Async<GlobalCryptoConfig> = Uninitialized,
val unverifiedDevicesInTheRoom: Async<Boolean> = Uninitialized,
val isLastAdmin: Boolean = false
) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View File

@@ -18,7 +18,6 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -29,7 +28,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.mapOptional
import org.matrix.android.sdk.flow.unwrap
@@ -125,12 +123,10 @@ class RoomAliasViewModel @AssistedInject constructor(
}
private fun observePowerLevel() {
PowerLevelsFlowFactory(room)
.createFlow()
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
room.flow().liveRoomPowerLevels()
.onEach { roomPowerLevels ->
val permissions = RoomAliasViewState.ActionPermissions(
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(
canChangeCanonicalAlias = roomPowerLevels.isUserAllowedToSend(
userId = session.myUserId,
isState = true,
eventType = EventType.STATE_ROOM_CANONICAL_ALIAS

View File

@@ -15,7 +15,6 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -29,7 +28,6 @@ import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
@@ -62,12 +60,10 @@ class RoomBannedMemberListViewModel @AssistedInject constructor(
)
}
val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow()
powerLevelsContentLive
.setOnEach {
val powerLevelsHelper = PowerLevelsHelper(it)
copy(canUserBan = powerLevelsHelper.isUserAbleToBan(session.myUserId))
val powerLevelsFlow = room.flow().liveRoomPowerLevels()
powerLevelsFlow
.setOnEach { roomPowerLevels ->
copy(canUserBan = roomPowerLevels.isUserAbleToBan(session.myUserId))
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2020-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.features.roomprofile.members
import javax.inject.Inject
class RoomMemberListComparator @Inject constructor() : Comparator<RoomMemberWithPowerLevel> {
override fun compare(leftRoomMember: RoomMemberWithPowerLevel?, rightRoomMember: RoomMemberWithPowerLevel?): Int {
return when (leftRoomMember) {
null ->
when (rightRoomMember) {
null -> 0
else -> 1
}
else ->
when (rightRoomMember) {
null -> -1
else ->
when {
leftRoomMember.powerLevel > rightRoomMember.powerLevel -> -1
leftRoomMember.powerLevel < rightRoomMember.powerLevel -> 1
leftRoomMember.summary.displayName.isNullOrBlank() ->
when {
rightRoomMember.summary.displayName.isNullOrBlank() -> {
// No display names, compare ids
leftRoomMember.summary.userId.compareTo(rightRoomMember.summary.userId)
}
else -> 1
}
else ->
when {
rightRoomMember.summary.displayName.isNullOrBlank() -> -1
else -> {
when (leftRoomMember.summary.displayName) {
rightRoomMember.summary.displayName ->
// Same display name, compare id
leftRoomMember.summary.userId.compareTo(rightRoomMember.summary.userId)
else ->
leftRoomMember.summary.displayName!!.compareTo(rightRoomMember.summary.displayName!!, true)
}
}
}
}
}
}
}
}

View File

@@ -16,11 +16,13 @@ import im.vector.app.core.extensions.join
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roomprofile.permissions.RoleFormatter
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@@ -29,7 +31,8 @@ class RoomMemberListController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val roomMemberSummaryFilter: RoomMemberSummaryFilter
private val roomMemberSummaryFilter: RoomMemberSummaryFilter,
private val roleFormatter: RoleFormatter,
) : TypedEpoxyController<RoomMemberListViewState>() {
interface Callback {
@@ -56,13 +59,13 @@ class RoomMemberListController @Inject constructor(
.orEmpty()
var threePidInvitesDone = filteredThreePidInvites.isEmpty()
for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) {
val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it) }
for ((category, roomMemberList) in roomMembersByPowerLevel) {
val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it.summary) }
if (filteredRoomMemberList.isEmpty()) {
continue
}
if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) {
if (category == RoomMemberListCategories.USER && !threePidInvitesDone) {
// If there is no regular invite, display threepid invite before the regular user
buildProfileSection(
stringProvider.getString(RoomMemberListCategories.INVITE.titleRes)
@@ -73,20 +76,20 @@ class RoomMemberListController @Inject constructor(
}
buildProfileSection(
stringProvider.getString(powerLevelCategory.titleRes)
stringProvider.getString(category.titleRes)
)
filteredRoomMemberList.join(
each = { _, roomMember ->
buildRoomMember(roomMember, powerLevelCategory, host, data)
buildRoomMember(roomMember, host, data)
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
id("divider_${roomMemberBefore.summary.userId}")
}
}
)
if (powerLevelCategory == RoomMemberListCategories.INVITE && !threePidInvitesDone) {
if (category == RoomMemberListCategories.INVITE && !threePidInvitesDone) {
// Display the threepid invite after the regular invite
dividerItem {
id("divider_threepidinvites")
@@ -108,24 +111,24 @@ class RoomMemberListController @Inject constructor(
}
private fun buildRoomMember(
roomMember: RoomMemberSummary,
powerLevelCategory: RoomMemberListCategories,
roomMember: RoomMemberWithPowerLevel,
host: RoomMemberListController,
data: RoomMemberListViewState
) {
val powerLabel = stringProvider.getString(powerLevelCategory.titleRes)
val role = Role.getSuggestedRole(roomMember.powerLevel)
val powerLabel = roleFormatter.format(role)
profileMatrixItemWithPowerLevelWithPresence {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
id(roomMember.summary.userId)
matrixItem(roomMember.summary.toMatrixItem())
avatarRenderer(host.avatarRenderer)
userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.summary.userId))
clickListener {
host.callback?.onRoomMemberClicked(roomMember)
host.callback?.onRoomMemberClicked(roomMember.summary)
}
showPresence(true)
userPresence(roomMember.userPresence)
ignoredUser(roomMember.userId in data.ignoredUserIds)
userPresence(roomMember.summary.userPresence)
ignoredUser(roomMember.summary.userId in data.ignoredUserIds)
powerLevelLabel(
span {
span(powerLabel) {

View File

@@ -16,7 +16,6 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -31,22 +30,19 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.RoomPowerLevels
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.mapOptional
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber
class RoomMemberListViewModel @AssistedInject constructor(
@Assisted initialState: RoomMemberListViewState,
private val roomMemberSummaryComparator: RoomMemberSummaryComparator,
private val roomMemberListComparator: RoomMemberListComparator,
private val session: Session
) :
VectorViewModel<RoomMemberListViewState, RoomMemberListAction, EmptyViewEvents>(initialState) {
@@ -75,14 +71,12 @@ class RoomMemberListViewModel @AssistedInject constructor(
memberships = Membership.activeMemberships()
}
val powerLevelsFlow = room.flow().liveRoomPowerLevels()
combine(
roomFlow.liveRoomMembers(roomMemberQueryParams),
roomFlow
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
) { roomMembers, powerLevelsContent ->
buildRoomMemberSummaries(powerLevelsContent, roomMembers)
powerLevelsFlow,
) { roomMembers, roomPowerLevels ->
buildRoomMemberSummaries(roomPowerLevels, roomMembers)
}
.execute { async ->
copy(roomMemberSummaries = async)
@@ -142,11 +136,11 @@ class RoomMemberListViewModel @AssistedInject constructor(
}
private fun observePowerLevel() {
PowerLevelsFlowFactory(room).createFlow()
.onEach {
room.flow().liveRoomPowerLevels()
.onEach { roomPowerLevels ->
val permissions = ActionPermissions(
canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId),
canRevokeThreePidInvite = PowerLevelsHelper(it).isUserAllowedToSend(
canInvite = roomPowerLevels.isUserAbleToInvite(session.myUserId),
canRevokeThreePidInvite = roomPowerLevels.isUserAllowedToSend(
userId = session.myUserId,
isState = true,
eventType = EventType.STATE_ROOM_THIRD_PARTY_INVITE
@@ -184,31 +178,34 @@ class RoomMemberListViewModel @AssistedInject constructor(
}
}
private fun buildRoomMemberSummaries(powerLevelsContent: PowerLevelsContent, roomMembers: List<RoomMemberSummary>): RoomMemberSummaries {
val admins = ArrayList<RoomMemberSummary>()
val moderators = ArrayList<RoomMemberSummary>()
val users = ArrayList<RoomMemberSummary>(roomMembers.size)
val customs = ArrayList<RoomMemberSummary>()
val invites = ArrayList<RoomMemberSummary>()
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
private fun buildRoomMemberSummaries(roomPowerLevels: RoomPowerLevels, roomMembers: List<RoomMemberSummary>): RoomMembersByRole {
val admins = ArrayList<RoomMemberWithPowerLevel>()
val moderators = ArrayList<RoomMemberWithPowerLevel>()
val users = ArrayList<RoomMemberWithPowerLevel>(roomMembers.size)
val invites = ArrayList<RoomMemberWithPowerLevel>()
roomMembers
.forEach { roomMember ->
val userRole = powerLevelsHelper.getUserRole(roomMember.userId)
val powerLevel = roomPowerLevels.getUserPowerLevel(roomMember.userId)
val userRole = Role.getSuggestedRole(powerLevel)
val roomMemberWithPowerLevel = RoomMemberWithPowerLevel(
powerLevel = powerLevel,
summary = roomMember,
)
when {
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)
roomMember.membership == Membership.INVITE -> invites.add(roomMemberWithPowerLevel)
userRole == Role.SuperAdmin ||
userRole == Role.Creator ||
userRole == Role.Admin -> admins.add(roomMemberWithPowerLevel)
userRole == Role.Moderator -> moderators.add(roomMemberWithPowerLevel)
userRole == Role.User -> users.add(roomMemberWithPowerLevel)
}
}
return listOf(
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)
RoomMemberListCategories.ADMIN to admins.sortedWith(roomMemberListComparator),
RoomMemberListCategories.MODERATOR to moderators.sortedWith(roomMemberListComparator),
RoomMemberListCategories.INVITE to invites.sortedWith(roomMemberListComparator),
RoomMemberListCategories.USER to users.sortedWith(roomMemberListComparator)
)
}

View File

@@ -18,11 +18,12 @@ import org.matrix.android.sdk.api.session.crypto.model.UserVerificationLevel
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
data class RoomMemberListViewState(
val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
val roomMemberSummaries: Async<RoomMembersByRole> = Uninitialized,
val areAllMembersLoaded: Boolean = false,
val ignoredUserIds: List<String> = emptyList(),
val filter: String = "",
@@ -41,12 +42,16 @@ data class ActionPermissions(
val canRevokeThreePidInvite: Boolean = false
)
typealias RoomMemberSummaries = List<Pair<RoomMemberListCategories, List<RoomMemberSummary>>>
data class RoomMemberWithPowerLevel(
val powerLevel: UserPowerLevel,
val summary: RoomMemberSummary,
)
typealias RoomMembersByRole = List<Pair<RoomMemberListCategories, List<RoomMemberWithPowerLevel>>>
enum class RoomMemberListCategories(@StringRes val titleRes: Int) {
ADMIN(CommonStrings.room_member_power_level_admins),
MODERATOR(CommonStrings.room_member_power_level_moderators),
CUSTOM(CommonStrings.room_member_power_level_custom),
INVITE(CommonStrings.room_member_power_level_invites),
USER(CommonStrings.room_member_power_level_users)
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright 2020-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.features.roomprofile.members
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import javax.inject.Inject
class RoomMemberSummaryComparator @Inject constructor() : Comparator<RoomMemberSummary> {
override fun compare(leftRoomMemberSummary: RoomMemberSummary?, rightRoomMemberSummary: RoomMemberSummary?): Int {
return when (leftRoomMemberSummary) {
null ->
when (rightRoomMemberSummary) {
null -> 0
else -> 1
}
else ->
when (rightRoomMemberSummary) {
null -> -1
else ->
when {
leftRoomMemberSummary.displayName.isNullOrBlank() ->
when {
rightRoomMemberSummary.displayName.isNullOrBlank() -> {
// No display names, compare ids
leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId)
}
else -> 1
}
else ->
when {
rightRoomMemberSummary.displayName.isNullOrBlank() -> -1
else -> {
when (leftRoomMemberSummary.displayName) {
rightRoomMemberSummary.displayName ->
// Same display name, compare id
leftRoomMemberSummary.userId.compareTo(rightRoomMemberSummary.userId)
else ->
leftRoomMemberSummary.displayName!!.compareTo(rightRoomMemberSummary.displayName!!, true)
}
}
}
}
}
}
}
}

View File

@@ -19,8 +19,9 @@ class RoleFormatter @Inject constructor(
return when (role) {
Role.Admin -> stringProvider.getString(CommonStrings.power_level_admin)
Role.Moderator -> stringProvider.getString(CommonStrings.power_level_moderator)
Role.Default -> stringProvider.getString(CommonStrings.power_level_default)
is Role.Custom -> stringProvider.getString(CommonStrings.power_level_custom, role.value)
Role.User -> stringProvider.getString(CommonStrings.power_level_default)
Role.Creator -> stringProvider.getString(CommonStrings.power_level_owner)
Role.SuperAdmin -> stringProvider.getString(CommonStrings.power_level_owner)
}
}
}

View File

@@ -8,9 +8,10 @@
package im.vector.app.features.roomprofile.permissions
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
sealed class RoomPermissionsAction : VectorViewModelAction {
object ToggleShowAllPermissions : RoomPermissionsAction()
data class UpdatePermission(val editablePermission: EditablePermission, val powerLevel: Int) : RoomPermissionsAction()
data class UpdatePermission(val editablePermission: EditablePermission, val powerLevel: UserPowerLevel.Value) : RoomPermissionsAction()
}

View File

@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.redactOrDefault
import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import javax.inject.Inject
class RoomPermissionsController @Inject constructor(
@@ -34,7 +35,7 @@ class RoomPermissionsController @Inject constructor(
) : TypedEpoxyController<RoomPermissionsViewState>() {
interface Callback {
fun onEditPermission(editablePermission: EditablePermission, currentRole: Role)
fun onEditPermission(editablePermission: EditablePermission, currentPowerLevel: UserPowerLevel.Value)
fun toggleShowAllPermissions()
}
@@ -165,7 +166,8 @@ class RoomPermissionsController @Inject constructor(
editable: Boolean,
isSpace: Boolean
) {
val currentRole = getCurrentRole(editablePermission, content)
val currentPowerLevel = getPowerLevel(editablePermission, content)
val currentRole = Role.getSuggestedRole(currentPowerLevel)
buildProfileAction(
id = editablePermission.labelResId.toString(),
title = stringProvider.getString(
@@ -177,12 +179,12 @@ class RoomPermissionsController @Inject constructor(
action = {
callback
?.takeIf { editable }
?.onEditPermission(editablePermission, currentRole)
?.onEditPermission(editablePermission, currentPowerLevel)
}
)
}
private fun getCurrentRole(editablePermission: EditablePermission, content: PowerLevelsContent): Role {
private fun getPowerLevel(editablePermission: EditablePermission, content: PowerLevelsContent): UserPowerLevel.Value {
val value = when (editablePermission) {
is EditablePermission.EventTypeEditablePermission -> content.events?.get(editablePermission.eventType) ?: content.stateDefaultOrDefault()
is EditablePermission.DefaultRole -> content.usersDefaultOrDefault()
@@ -194,20 +196,6 @@ class RoomPermissionsController @Inject constructor(
is EditablePermission.RemoveMessagesSentByOthers -> content.redactOrDefault()
is EditablePermission.NotifyEveryone -> content.notificationLevel(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY)
}
return Role.fromValue(
value,
when (editablePermission) {
is EditablePermission.EventTypeEditablePermission -> content.stateDefaultOrDefault()
is EditablePermission.DefaultRole -> Role.Default.value
is EditablePermission.SendMessages -> Role.Default.value
is EditablePermission.InviteUsers -> Role.Moderator.value
is EditablePermission.ChangeSettings -> Role.Moderator.value
is EditablePermission.KickUsers -> Role.Moderator.value
is EditablePermission.BanUsers -> Role.Moderator.value
is EditablePermission.RemoveMessagesSentByOthers -> Role.Moderator.value
is EditablePermission.NotifyEveryone -> Role.Moderator.value
}
)
return UserPowerLevel.Value(value)
}
}

View File

@@ -26,7 +26,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.lib.strings.CommonStrings
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@@ -93,8 +93,8 @@ class RoomPermissionsFragment :
}
}
override fun onEditPermission(editablePermission: EditablePermission, currentRole: Role) {
EditPowerLevelDialogs.showChoice(requireActivity(), editablePermission.labelResId, currentRole) { newPowerLevel ->
override fun onEditPermission(editablePermission: EditablePermission, currentPowerLevel: UserPowerLevel.Value) {
EditPowerLevelDialogs.showChoice(requireActivity(), editablePermission.labelResId, currentPowerLevel) { newPowerLevel ->
viewModel.handle(RoomPermissionsAction.UpdatePermission(editablePermission, newPowerLevel))
}
}

View File

@@ -9,13 +9,13 @@ package im.vector.app.features.roomprofile.permissions
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
@@ -59,21 +58,24 @@ class RoomPermissionsViewModel @AssistedInject constructor(
}
private fun observePowerLevel() {
PowerLevelsFlowFactory(room)
.createFlow()
.onEach { powerLevelContent ->
val powerLevelsHelper = PowerLevelsHelper(powerLevelContent)
room.flow().liveRoomPowerLevels()
.onEach { roomPowerLevels ->
val permissions = RoomPermissionsViewState.ActionPermissions(
canChangePowerLevels = powerLevelsHelper.isUserAllowedToSend(
canChangePowerLevels = roomPowerLevels.isUserAllowedToSend(
userId = session.myUserId,
isState = true,
eventType = EventType.STATE_ROOM_POWER_LEVELS
)
)
val powerLevelsContent = roomPowerLevels.powerLevelsContent
setState {
copy(
actionPermissions = permissions,
currentPowerLevelsContent = Success(powerLevelContent)
currentPowerLevelsContent = if (powerLevelsContent != null) {
Success(powerLevelsContent)
} else {
Uninitialized
}
)
}
}.launchIn(viewModelScope)
@@ -94,26 +96,26 @@ class RoomPermissionsViewModel @AssistedInject constructor(
private fun updatePermission(action: RoomPermissionsAction.UpdatePermission) {
withState { state ->
val currentPowerLevel = state.currentPowerLevelsContent.invoke() ?: return@withState
val currentPowerLevelsContent = state.currentPowerLevelsContent.invoke() ?: return@withState
postLoading(true)
viewModelScope.launch {
try {
val newPowerLevelsContent = when (action.editablePermission) {
is EditablePermission.EventTypeEditablePermission -> currentPowerLevel.copy(
events = currentPowerLevel.events.orEmpty().toMutableMap().apply {
put(action.editablePermission.eventType, action.powerLevel)
is EditablePermission.EventTypeEditablePermission -> currentPowerLevelsContent.copy(
events = currentPowerLevelsContent.events.orEmpty().toMutableMap().apply {
put(action.editablePermission.eventType, action.powerLevel.value)
}
)
is EditablePermission.DefaultRole -> currentPowerLevel.copy(usersDefault = action.powerLevel)
is EditablePermission.SendMessages -> currentPowerLevel.copy(eventsDefault = action.powerLevel)
is EditablePermission.InviteUsers -> currentPowerLevel.copy(invite = action.powerLevel)
is EditablePermission.ChangeSettings -> currentPowerLevel.copy(stateDefault = action.powerLevel)
is EditablePermission.KickUsers -> currentPowerLevel.copy(kick = action.powerLevel)
is EditablePermission.BanUsers -> currentPowerLevel.copy(ban = action.powerLevel)
is EditablePermission.RemoveMessagesSentByOthers -> currentPowerLevel.copy(redact = action.powerLevel)
is EditablePermission.NotifyEveryone -> currentPowerLevel.copy(
notifications = currentPowerLevel.notifications.orEmpty().toMutableMap().apply {
put(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY, action.powerLevel)
is EditablePermission.DefaultRole -> currentPowerLevelsContent.copy(usersDefault = action.powerLevel.value)
is EditablePermission.SendMessages -> currentPowerLevelsContent.copy(eventsDefault = action.powerLevel.value)
is EditablePermission.InviteUsers -> currentPowerLevelsContent.copy(invite = action.powerLevel.value)
is EditablePermission.ChangeSettings -> currentPowerLevelsContent.copy(stateDefault = action.powerLevel.value)
is EditablePermission.KickUsers -> currentPowerLevelsContent.copy(kick = action.powerLevel.value)
is EditablePermission.BanUsers -> currentPowerLevelsContent.copy(ban = action.powerLevel.value)
is EditablePermission.RemoveMessagesSentByOthers -> currentPowerLevelsContent.copy(redact = action.powerLevel.value)
is EditablePermission.NotifyEveryone -> currentPowerLevelsContent.copy(
notifications = currentPowerLevelsContent.notifications.orEmpty().toMutableMap().apply {
put(PowerLevelsContent.NOTIFICATIONS_ROOM_KEY, action.powerLevel.value)
}
)
}

View File

@@ -15,7 +15,6 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
@@ -32,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.mapOptional
import org.matrix.android.sdk.flow.unwrap
@@ -115,28 +113,26 @@ class RoomSettingsViewModel @AssistedInject constructor(
)
}
val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow()
powerLevelsContentLive
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
val powerLevelsFlow = room.flow().liveRoomPowerLevels()
powerLevelsFlow
.onEach { roomPowerLevels ->
val permissions = RoomSettingsViewState.ActionPermissions(
canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
canChangeHistoryVisibility = powerLevelsHelper.isUserAllowedToSend(
canChangeAvatar = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
canChangeName = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
canChangeTopic = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
canChangeHistoryVisibility = roomPowerLevels.isUserAllowedToSend(
session.myUserId, true,
EventType.STATE_ROOM_HISTORY_VISIBILITY
),
canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(
canChangeJoinRule = roomPowerLevels.isUserAllowedToSend(
session.myUserId, true,
EventType.STATE_ROOM_JOIN_RULES
) &&
powerLevelsHelper.isUserAllowedToSend(
roomPowerLevels.isUserAllowedToSend(
session.myUserId, true,
EventType.STATE_ROOM_GUEST_ACCESS
),
canAddChildren = powerLevelsHelper.isUserAllowedToSend(
canAddChildren = roomPowerLevels.isUserAllowedToSend(
session.myUserId, true,
EventType.STATE_SPACE_CHILD
)

View File

@@ -20,7 +20,6 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -32,7 +31,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
@@ -72,22 +70,20 @@ class SpaceMenuViewModel @AssistedInject constructor(
}
}.launchIn(viewModelScope)
PowerLevelsFlowFactory(room)
.createFlow()
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
room.flow().liveRoomPowerLevels()
.onEach { roomPowerLevels ->
val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId)
val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD)
val canInvite = roomPowerLevels.isUserAbleToInvite(session.myUserId)
val canAddChild = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD)
val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR)
val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME)
val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC)
val canChangeAvatar = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR)
val canChangeName = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME)
val canChangeTopic = roomPowerLevels.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC)
val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
val isAdmin = roomPowerLevels.getSuggestedRole(session.myUserId) == Role.Admin
val otherAdminCount = roomSummary?.otherMemberIds
?.map { powerLevelsHelper.getUserRole(it) }
?.count { it is Role.Admin }
?.map { roomPowerLevels.getSuggestedRole(it) }
?.count { it == Role.Admin }
?: 0
val isLastAdmin = isAdmin && otherAdminCount == 0

View File

@@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.powerlevels.UserPowerLevel
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import timber.log.Timber
import javax.inject.Inject
@@ -65,7 +65,7 @@ class CreateSpaceViewModelTask @Inject constructor(
if (params.isPublic) {
this.roomAliasName = params.spaceAlias
this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
invite = Role.Default.value
invite = UserPowerLevel.User.value
)
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
@@ -79,7 +79,7 @@ class CreateSpaceViewModelTask @Inject constructor(
}
)
this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
invite = Role.Moderator.value
invite = UserPowerLevel.Moderator.value
)
}
})

View File

@@ -21,7 +21,6 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.analytics.plan.JoinedRoom
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -36,7 +35,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
@@ -96,16 +94,14 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
private fun observePermissions() {
val room = session.getRoom(initialState.spaceId) ?: return
val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow()
val powerLevelsFlow = room.flow().liveRoomPowerLevels()
powerLevelsContentLive
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
powerLevelsFlow
.onEach { roomPowerLevels ->
setState {
copy(
canAddRooms = powerLevelsHelper.isUserAllowedToSend(
session.myUserId, true,
EventType.STATE_SPACE_CHILD
canAddRooms = roomPowerLevels.isUserAllowedToSend(
userId = session.myUserId, isState = true, eventType = EventType.STATE_SPACE_CHILD
)
)
}

View File

@@ -50,27 +50,12 @@ class SpaceLeaveAdvancedFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
controller.listener = this
withState(viewModel) { state ->
setupToolbar(views.toolbar)
.setSubtitle(state.spaceSummary?.name)
.allowBack()
state.spaceSummary?.let { summary ->
val warningMessage: CharSequence? = when {
summary.otherMemberIds.isEmpty() -> getString(CommonStrings.space_leave_prompt_msg_only_you)
state.isLastAdmin -> getString(CommonStrings.space_leave_prompt_msg_as_admin)
!summary.isPublic -> getString(CommonStrings.space_leave_prompt_msg_private)
else -> null
}
views.spaceLeavePromptDescription.isVisible = warningMessage != null
views.spaceLeavePromptDescription.text = warningMessage
}
views.spaceLeavePromptTitle.text = getString(CommonStrings.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
}
views.roomList.configureWith(controller)
@@ -107,6 +92,19 @@ class SpaceLeaveAdvancedFragment :
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
state.spaceSummary?.let { summary ->
val warningMessage: CharSequence? = when {
summary.otherMemberIds.isEmpty() -> getString(CommonStrings.space_leave_prompt_msg_only_you)
state.isLastAdmin -> getString(CommonStrings.space_leave_prompt_msg_as_admin)
!summary.isPublic -> getString(CommonStrings.space_leave_prompt_msg_private)
else -> null
}
views.spaceLeavePromptDescription.isVisible = warningMessage != null
views.spaceLeavePromptDescription.text = warningMessage
}
views.spaceLeavePromptTitle.text = getString(CommonStrings.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
if (state.isFilteringEnabled) {
views.appBarLayout.setExpanded(false)
}

View File

@@ -20,22 +20,16 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.isLastAdminFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import okhttp3.internal.toImmutableList
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
@@ -49,22 +43,13 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
init {
val space = session.getRoom(initialState.spaceId)
space?.isLastAdminFlow(session.myUserId)
?.onEach { isLastAdmin ->
setState { copy(isLastAdmin = isLastAdmin) }
}?.launchIn(viewModelScope)
val spaceSummary = space?.roomSummary()
val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
powerLevelsEvent?.content?.toModel<PowerLevelsContent>()?.let { powerLevelsContent ->
val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin
val otherAdminCount = spaceSummary?.otherMemberIds
?.map { powerLevelsHelper.getUserRole(it) }
?.count { it is Role.Admin }
?: 0
val isLastAdmin = isAdmin && otherAdminCount == 0
setState {
copy(isLastAdmin = isLastAdmin)
}
}
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)
?.flow()

View File

@@ -54,7 +54,7 @@ class SpacePeopleListController @Inject constructor(
memberSummaries.forEach { memberEntry ->
val filtered = memberEntry.second
.filter { roomMemberSummaryFilter.test(it) }
.filter { roomMemberSummaryFilter.test(it.summary) }
if (filtered.isNotEmpty()) {
dividerItem {
id("divider_type_${memberEntry.first.titleRes}")
@@ -65,10 +65,10 @@ class SpacePeopleListController @Inject constructor(
.join(
each = { _, roomMember ->
profileMatrixItemWithPowerLevel {
id(roomMember.userId)
matrixItem(roomMember.toMatrixItem())
id(roomMember.summary.userId)
matrixItem(roomMember.summary.toMatrixItem())
avatarRenderer(host.avatarRenderer)
userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
userVerificationLevel(data.trustLevelMap.invoke()?.get(roomMember.summary.userId))
.apply {
val pl = host.toPowerLevelLabel(memberEntry.first)
if (memberEntry.first == RoomMemberListCategories.INVITE) {
@@ -106,13 +106,13 @@ class SpacePeopleListController @Inject constructor(
}
clickListener {
host.listener?.onSpaceMemberClicked(roomMember)
host.listener?.onSpaceMemberClicked(roomMember.summary)
}
}
},
between = { _, roomMemberBefore ->
dividerItem {
id("divider_${roomMemberBefore.userId}")
id("divider_${roomMemberBefore.summary.userId}")
}
}
)

View File

@@ -16,14 +16,13 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
class ShareSpaceViewModel @AssistedInject constructor(
@Assisted private val initialState: ShareSpaceViewState,
@@ -50,13 +49,11 @@ class ShareSpaceViewModel @AssistedInject constructor(
private fun observePowerLevel() {
val room = session.getRoom(initialState.spaceId) ?: return
PowerLevelsFlowFactory(room)
.createFlow()
.onEach { powerLevelContent ->
val powerLevelsHelper = PowerLevelsHelper(powerLevelContent)
room.flow().liveRoomPowerLevels()
.onEach { roomPowerLevels ->
setState {
copy(
canInviteByMxId = powerLevelsHelper.isUserAbleToInvite(session.myUserId)
canInviteByMxId = roomPowerLevels.isUserAbleToInvite(session.myUserId)
)
}
}

View File

@@ -25,18 +25,13 @@ import im.vector.lib.multipicker.utils.toMultiPickerAudioType
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.getRoomPowerLevels
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber
@@ -139,12 +134,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
@VisibleForTesting
fun assertHasEnoughPowerLevels(room: Room) {
val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
?.content
?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) {
val roomPowerLevels = room.getRoomPowerLevels()
if (!roomPowerLevels.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO)) {
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission")
throw VoiceBroadcastFailure.RecordingError.NoPermission
}

View File

@@ -23,12 +23,10 @@ import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getRoomPowerLevels
import org.matrix.android.sdk.api.session.room.getStateEvent
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator
import org.matrix.android.sdk.api.util.JsonDict
import timber.log.Timber
@@ -146,13 +144,8 @@ class WidgetPostAPIHandler @AssistedInject constructor(
Timber.d("## canSendEvent() : eventType $eventType isState $isState")
val powerLevelsEvent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
val canSend = if (powerLevelsContent == null) {
false
} else {
PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(session.myUserId, isState, eventType)
}
val roomPowerLevels = room.getRoomPowerLevels()
val canSend = roomPowerLevels.isUserAllowedToSend(session.myUserId, isState, eventType)
if (canSend) {
Timber.d("## canSendEvent() returns true")
widgetPostAPIMediator.sendBoolResponse(true, eventData)

View File

@@ -26,16 +26,10 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.mapOptional
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber
import javax.net.ssl.HttpsURLConnection
@@ -102,11 +96,9 @@ class WidgetViewModel @AssistedInject constructor(
if (room == null) {
return
}
room.flow().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap()
.map {
PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, null)
room.flow().liveRoomPowerLevels()
.map { roomPowerLevels ->
roomPowerLevels.isUserAllowedToSend(session.myUserId, true, null)
}
.setOnEach {
copy(canManageWidgets = it)

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/powerLevelRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
@@ -7,54 +9,32 @@
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding">
<RadioGroup
android:id="@+id/powerLevelRadioGroup"
android:layout_width="match_parent"
<RadioButton
android:id="@+id/powerLevelOwnerRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
android:text="@string/power_level_owner"
android:textColor="?vctr_content_primary" />
<RadioButton
android:id="@+id/powerLevelAdminRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_admin"
android:textColor="?vctr_content_primary" />
<RadioButton
android:id="@+id/powerLevelModeratorRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_moderator"
android:textColor="?vctr_content_primary" />
<RadioButton
android:id="@+id/powerLevelDefaultRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_default"
android:textColor="?vctr_content_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="?vctr_content_primary" />
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/powerLevelCustomEditLayout"
android:layout_width="match_parent"
<RadioButton
android:id="@+id/powerLevelAdminRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/power_level_custom_no_value">
android:text="@string/power_level_admin"
android:textColor="?vctr_content_primary" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/powerLevelCustomEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number|numberSigned" />
<RadioButton
android:id="@+id/powerLevelModeratorRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_moderator"
android:textColor="?vctr_content_primary" />
</com.google.android.material.textfield.TextInputLayout>
<RadioButton
android:id="@+id/powerLevelDefaultRadio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/power_level_default"
android:textColor="?vctr_content_primary" />
</LinearLayout>
</RadioGroup>

View File

@@ -11,9 +11,9 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.features.roomprofile.RoomProfileArgs
import im.vector.app.features.roomprofile.members.RoomMemberListComparator
import im.vector.app.features.roomprofile.members.RoomMemberListViewModel
import im.vector.app.features.roomprofile.members.RoomMemberListViewState
import im.vector.app.features.roomprofile.members.RoomMemberSummaryComparator
import im.vector.app.test.test
import im.vector.app.test.testCoroutineDispatchers
import io.mockk.coEvery
@@ -266,7 +266,7 @@ class MemberListViewModelTest {
private fun createViewModel(): RoomMemberListViewModel {
return RoomMemberListViewModel(
RoomMemberListViewState(args),
RoomMemberSummaryComparator(),
RoomMemberListComparator(),
fakeSession,
)
}

View File

@@ -14,6 +14,7 @@ import im.vector.app.features.session.coroutineScope
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeLocationManager
import im.vector.app.test.fakes.FakePermissionChecker
import im.vector.app.test.fixtures.aBuildMeta
import im.vector.app.test.test
import io.mockk.every
@@ -48,7 +49,7 @@ class LocationTrackerTest {
@Before
fun setUp() {
mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt")
locationTracker = LocationTracker(fakeContext.instance, fakeActiveSessionHolder.instance, aBuildMeta())
locationTracker = LocationTracker(fakeContext.instance, fakeActiveSessionHolder.instance, aBuildMeta(), FakePermissionChecker())
fakeLocationManager.givenRemoveUpdates(locationTracker)
}

View File

@@ -12,6 +12,7 @@ import im.vector.app.features.location.LocationData
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.test.fakes.FakeLocationSharingServiceConnection
import im.vector.app.test.fakes.FakeLocationTracker
import im.vector.app.test.fakes.FakePermissionChecker
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.test
import io.mockk.every
@@ -38,6 +39,7 @@ class LiveLocationMapViewModelTest {
private val fakeLocationSharingServiceConnection = FakeLocationSharingServiceConnection()
private val fakeStopLiveLocationShareUseCase = mockk<StopLiveLocationShareUseCase>()
private val fakeLocationTracker = FakeLocationTracker()
private val fakePermissionChecker = FakePermissionChecker()
private fun createViewModel(): LiveLocationMapViewModel {
return LiveLocationMapViewModel(
@@ -47,6 +49,7 @@ class LiveLocationMapViewModelTest {
locationSharingServiceConnection = fakeLocationSharingServiceConnection.instance,
stopLiveLocationShareUseCase = fakeStopLiveLocationShareUseCase,
locationTracker = fakeLocationTracker.instance,
permissionChecker = fakePermissionChecker
)
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2022-2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.test.fakes
import im.vector.app.core.utils.PermissionChecker
class FakePermissionChecker(val permissionResult: Boolean = true) : PermissionChecker {
override fun checkPermission(vararg permissions: String): Boolean {
return permissionResult
}
}