ui: allow to change member role in channel (#7096)

This commit is contained in:
spaced4ndy
2026-06-22 12:53:05 +00:00
committed by GitHub
parent 0e09b38ea6
commit 117ed75d40
17 changed files with 85 additions and 62 deletions
@@ -2617,8 +2617,15 @@ data class GroupMember (
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
if (memberRole == GroupMemberRole.Relay || !canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null
else if (groupInfo.useRelays && !groupInfo.isOwner) null
else groupInfo.membership.memberRole.let { userRole ->
GroupMemberRole.selectableRoles.filter { it <= userRole }
if (groupInfo.useRelays)
// TODO [relays]: for now owners can only set observer/member in channels.
// Restore the full Owner-excluded picker when moderator/admin promotion is supported:
// GroupMemberRole.selectableRoles.filter { it <= userRole && it != GroupMemberRole.Owner }
listOf(GroupMemberRole.Observer, GroupMemberRole.Member)
else
GroupMemberRole.selectableRoles.filter { it <= userRole }
}
fun canBlockForAll(groupInfo: GroupInfo): Boolean {
@@ -2696,11 +2703,11 @@ enum class GroupMemberRole(val memberRole: String) {
val selectableRoles: List<GroupMemberRole> = listOf(Observer, Member, Moderator, Admin, Owner)
}
val text: String get() = when (this) {
fun text(isChannel: Boolean): String = when (this) {
Relay -> generalGetString(MR.strings.group_member_role_relay)
Observer -> generalGetString(MR.strings.group_member_role_observer)
Observer -> generalGetString(if (isChannel) MR.strings.group_member_role_observer_channel else MR.strings.group_member_role_observer)
Author -> generalGetString(MR.strings.group_member_role_author)
Member -> generalGetString(MR.strings.group_member_role_member)
Member -> generalGetString(if (isChannel) MR.strings.group_member_role_member_channel else MR.strings.group_member_role_member)
Moderator -> generalGetString(MR.strings.group_member_role_moderator)
Admin -> generalGetString(MR.strings.group_member_role_admin)
Owner -> generalGetString(MR.strings.group_member_role_owner)
@@ -5094,13 +5101,13 @@ sealed class RcvGroupEvent() {
is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName)
is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted)
is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left)
is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text(isChannel = isChannel))
is MemberBlocked -> if (blocked) {
String.format(generalGetString(MR.strings.rcv_group_event_member_blocked), profile.profileViewName)
} else {
String.format(generalGetString(MR.strings.rcv_group_event_member_unblocked), profile.profileViewName)
}
is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text)
is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text(isChannel = isChannel))
is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName)
is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted)
is GroupDeleted -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_channel_deleted else MR.strings.rcv_group_event_group_deleted)
@@ -5139,8 +5146,8 @@ sealed class SndGroupEvent() {
val text: String get() = text(isChannel = false)
fun text(isChannel: Boolean): String = when (this) {
is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text)
is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text)
is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text(isChannel = isChannel))
is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text(isChannel = isChannel))
is MemberBlocked -> if (blocked) {
String.format(generalGetString(MR.strings.snd_group_event_member_blocked), profile.profileViewName)
} else {
@@ -2035,7 +2035,7 @@ fun BoxScope.ChatItemsList(
val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
Text(
member.memberRole.text,
member.memberRole.text(isChannel = chatInfo.isChannel),
Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
fontSize = 13.5.sp,
fontWeight = FontWeight.Medium,
@@ -228,7 +228,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
) {
val values = GroupMemberRole.selectableRoles
.filter { it <= groupInfo.membership.memberRole }
.map { it to it.text }
.map { it to it.text(isChannel = groupInfo.isChannel) }
ExposedDropDownSettingRow(
generalGetString(MR.strings.new_member_role),
values,
@@ -46,7 +46,7 @@ fun ChannelMembersView(
val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong()
SectionView(title = subscriberCountStr(subscriberCount)) {
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
ChannelMemberRow(groupInfo.membership, user = true, showRole = true)
ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel)
}
members.forEachIndexed { index, member ->
Divider()
@@ -55,7 +55,7 @@ fun ChannelMembersView(
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner)
ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner, isChannel = groupInfo.isChannel)
}
}
}
@@ -71,7 +71,7 @@ fun ChannelMembersView(
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
ChannelMemberRow(member, user = false, showRole = false)
ChannelMemberRow(member, user = false, showRole = false, isChannel = groupInfo.isChannel)
}
}
}
@@ -81,7 +81,7 @@ fun ChannelMembersView(
}
@Composable
private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boolean) {
private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boolean, isChannel: Boolean) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
@@ -112,7 +112,7 @@ private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boole
}
if (showRole) {
Text(
member.memberRole.text,
member.memberRole.text(isChannel = isChannel),
color = MaterialTheme.colors.secondary
)
}
@@ -731,7 +731,7 @@ fun ModalData.GroupChatInfoLayout(
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
MemberRow(groupInfo.membership, user = true)
MemberRow(groupInfo.membership, user = true, isChannel = groupInfo.isChannel)
}
}
}
@@ -765,7 +765,7 @@ fun ModalData.GroupChatInfoLayout(
val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp)
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu)
Box(Modifier.padding(start = selectionOffset)) {
MemberRow(member)
MemberRow(member, isChannel = groupInfo.isChannel)
}
}
}
@@ -1057,7 +1057,7 @@ private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialThem
}
@Composable
fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false) {
fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false, isChannel: Boolean = false) {
@Composable
fun MemberInfo() {
if (member.blocked) {
@@ -1065,7 +1065,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr
} else {
val role = member.memberRole
if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Moderator, GroupMemberRole.Observer)) {
Text(role.text, color = MaterialTheme.colors.secondary)
Text(role.text(isChannel = isChannel), color = MaterialTheme.colors.secondary)
}
}
}
@@ -308,7 +308,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text }
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text(isChannel = groupInfo.isChannel) }
ExposedDropDownSettingRow(
generalGetString(MR.strings.initial_member_role),
values,
@@ -596,15 +596,11 @@ fun GroupMemberInfoLayout(
else if (groupInfo.businessChat == null) MR.strings.info_row_group
else MR.strings.info_row_chat
InfoRow(stringResource(titleId), groupInfo.displayName)
if (!groupInfo.useRelays) {
val roles = remember { member.canChangeRoleTo(groupInfo) }
if (roles != null) {
RoleSelectionRow(roles, newRole, onRoleSelected)
} else {
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text)
}
val roles = remember { member.canChangeRoleTo(groupInfo) }
if (roles != null) {
RoleSelectionRow(roles, newRole, onRoleSelected, groupInfo.isChannel)
} else {
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text)
InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text(isChannel = groupInfo.isChannel))
}
val relayLink = member.relayLink
if (relayLink != null) {
@@ -888,14 +884,15 @@ fun ConnectViaAddressButton(onClick: () -> Unit) {
private fun RoleSelectionRow(
roles: List<GroupMemberRole>,
selectedRole: MutableState<GroupMemberRole>,
onSelected: (GroupMemberRole) -> Unit
onSelected: (GroupMemberRole) -> Unit,
isChannel: Boolean
) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val values = remember { roles.map { it to it.text } }
val values = remember { roles.map { it to it.text(isChannel = isChannel) } }
ExposedDropDownSettingRow(
generalGetString(MR.strings.change_role),
values,
@@ -956,12 +953,14 @@ fun updateMemberRoleDialog(
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_member_role_question),
text = if (memberCurrent) {
if (groupInfo.businessChat == null)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
if (groupInfo.isChannel)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_channel), newRole.text(isChannel = groupInfo.isChannel))
else if (groupInfo.businessChat == null)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text(isChannel = groupInfo.isChannel))
else
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text(isChannel = groupInfo.isChannel))
} else
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text),
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text(isChannel = groupInfo.isChannel)),
confirmText = generalGetString(MR.strings.change_verb),
onDismiss = onDismiss,
onConfirm = onConfirm,
@@ -977,9 +976,9 @@ fun updateMembersRoleDialog(
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_member_role_question),
text = if (groupInfo.businessChat == null)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text)
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text(isChannel = groupInfo.isChannel))
else
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text),
String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text(isChannel = groupInfo.isChannel)),
confirmText = generalGetString(MR.strings.change_verb),
onConfirm = onConfirm,
)
@@ -150,7 +150,7 @@ private fun ModalData.MemberSupportViewLayout(
) {
Box(contentAlignment = Alignment.CenterStart) {
DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu)
SupportChatRow(member)
SupportChatRow(member, isChannel = groupInfo.isChannel)
}
}
}
@@ -163,7 +163,7 @@ private fun ModalData.MemberSupportViewLayout(
}
@Composable
fun SupportChatRow(member: GroupMember) {
fun SupportChatRow(member: GroupMember, isChannel: Boolean) {
fun memberStatus(): String {
return if (member.activeConn?.connStatus is ConnStatus.Failed) {
generalGetString(MR.strings.member_info_member_failed)
@@ -174,7 +174,7 @@ fun SupportChatRow(member: GroupMember) {
} else if (member.memberPending) {
member.memberStatus.text
} else {
member.memberRole.text
member.memberRole.text(isChannel = isChannel)
}
}
@@ -1850,8 +1850,10 @@
<!-- GroupMemberRole -->
<string name="group_member_role_observer">observer</string>
<string name="group_member_role_observer_channel">subscriber</string>
<string name="group_member_role_author">author</string>
<string name="group_member_role_member">member</string>
<string name="group_member_role_member_channel">contributor</string>
<string name="group_member_role_moderator">moderator</string>
<string name="group_member_role_admin">admin</string>
<string name="group_member_role_owner">owner</string>
@@ -2043,9 +2045,10 @@
<string name="change_role">Change role</string>
<string name="change_verb">Change</string>
<string name="switch_verb">Switch</string>
<string name="change_member_role_question">Change group role?</string>
<string name="change_member_role_question">Change role?</string>
<string name="member_role_will_be_changed_with_notification">The role will be changed to "%s". Everyone in the group will be notified.</string>
<string name="member_role_will_be_changed_with_notification_chat">The role will be changed to "%s". Everyone in the chat will be notified.</string>
<string name="member_role_will_be_changed_with_notification_channel">The role will be changed to "%s". Everyone in the channel will be notified.</string>
<string name="member_role_will_be_changed_with_invitation">The role will be changed to "%s". The member will receive a new invitation.</string>
<string name="connect_via_member_address_alert_title">Connect directly?</string>
<string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string>