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
+1 -1
View File
@@ -1999,7 +1999,7 @@ struct ChatView: View {
let (name, role) = if ci.meta.showGroupAsSender {
(groupInfo.chatViewName, NSLocalizedString("group", comment: "shown on group welcome message"))
} else {
(member.chatViewName, member.memberRole.text)
(member.chatViewName, member.memberRole.text(isChannel: groupInfo.isChannel))
}
Group {
if #available(iOS 16.0, *) {
@@ -183,7 +183,7 @@ struct AddGroupMembersViewCommon: View {
private func rolePicker() -> some View {
Picker("New member role", selection: $selectedRole) {
ForEach(GroupMemberRole.supportedRoles.filter({ $0 <= groupInfo.membership.memberRole })) { role in
Text(role.text)
Text(role.text(isChannel: groupInfo.isChannel))
}
}
.frame(height: 36)
@@ -66,7 +66,7 @@ struct ChannelMembersView: View {
}
Spacer()
if showRole {
Text(member.memberRole.text)
Text(member.memberRole.text(isChannel: groupInfo.isChannel))
.foregroundColor(theme.colors.secondary)
}
}
@@ -581,7 +581,7 @@ struct GroupChatInfoView: View {
} else {
let role = member.memberRole
if [.owner, .admin, .moderator, .observer].contains(role) {
Text(member.memberRole.text)
Text(member.memberRole.text(isChannel: groupInfo.isChannel))
.foregroundColor(theme.colors.secondary)
}
}
@@ -84,7 +84,7 @@ struct GroupLinkView: View {
if !isChannel {
Picker("Initial role", selection: $groupLinkMemberRole) {
ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
Text(role.text)
Text(role.text(isChannel: isChannel))
}
}
.frame(height: 36)
@@ -178,15 +178,15 @@ struct GroupMemberInfoView: View {
let label: LocalizedStringKey = groupInfo.useRelays ? "Channel" : groupInfo.businessChat == nil ? "Group" : "Chat"
infoRow(label, groupInfo.displayName)
if !groupInfo.useRelays, let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
Picker("Change role", selection: $newRole) {
ForEach(roles) { role in
Text(role.text)
Text(role.text(isChannel: groupInfo.isChannel))
}
}
.frame(height: 36)
} else {
infoRow("Role", member.memberRole.text)
infoRow("Role", member.memberRole.text(isChannel: groupInfo.isChannel))
}
if let link = member.relayLink {
infoRow("Relay link", String.localizedStringWithFormat(NSLocalizedString("via %@", comment: "relay hostname"), hostFromRelayLink(link)))
@@ -727,15 +727,17 @@ struct GroupMemberInfoView: View {
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
Alert(
title: Text("Change member role?"),
title: Text("Change role?"),
message: (
mem.memberCurrent
? (
groupInfo.businessChat == nil
? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.")
: Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.")
groupInfo.isChannel
? Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All subscribers will be notified.")
: groupInfo.businessChat == nil
? Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All group members will be notified.")
: Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All chat members will be notified.")
)
: Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.")
: Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". The member will receive a new invitation.")
),
primaryButton: .default(Text("Change")) {
Task {
@@ -205,7 +205,7 @@ struct MemberSupportView: View {
} else if member.memberPending {
return member.memberStatus.text
} else {
return LocalizedStringKey(member.memberRole.text)
return LocalizedStringKey(member.memberRole.text(isChannel: groupInfo.isChannel))
}
}
+20 -8
View File
@@ -2981,8 +2981,16 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
if memberRole == .relay || !canBeRemoved(groupInfo: groupInfo) || memberStatus == .memRemoved || memberStatus == .memLeft || memberPending { return nil }
if groupInfo.useRelays && !groupInfo.isOwner { return nil }
let userRole = groupInfo.membership.memberRole
return GroupMemberRole.supportedRoles.filter { $0 <= 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:
// return GroupMemberRole.supportedRoles.filter { $0 <= userRole && $0 != .owner }
return [.observer, .member]
} else {
return GroupMemberRole.supportedRoles.filter { $0 <= userRole }
}
}
public func canBlockForAll(groupInfo: GroupInfo) -> Bool {
@@ -3070,12 +3078,16 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod
public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .moderator, .admin, .owner]
public var text: String {
public func text(isChannel: Bool) -> String {
switch self {
case .relay: return NSLocalizedString("relay", comment: "member role")
case .observer: return NSLocalizedString("observer", comment: "member role")
case .observer: return isChannel
? NSLocalizedString("subscriber", comment: "member role")
: NSLocalizedString("observer", comment: "member role")
case .author: return NSLocalizedString("author", comment: "member role")
case .member: return NSLocalizedString("member", comment: "member role")
case .member: return isChannel
? NSLocalizedString("contributor", comment: "member role")
: NSLocalizedString("member", comment: "member role")
case .moderator: return NSLocalizedString("moderator", comment: "member role")
case .admin: return NSLocalizedString("admin", comment: "member role")
case .owner: return NSLocalizedString("owner", comment: "member role")
@@ -5596,7 +5608,7 @@ public enum RcvGroupEvent: Decodable, Hashable {
case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item")
case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
case let .memberRole(_, profile, role):
return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text(isChannel: isChannel))
case let .memberBlocked(_, profile, blocked):
if blocked {
return String.localizedStringWithFormat(NSLocalizedString("blocked %@", comment: "rcv group event chat item"), profile.profileViewName)
@@ -5604,7 +5616,7 @@ public enum RcvGroupEvent: Decodable, Hashable {
return String.localizedStringWithFormat(NSLocalizedString("unblocked %@", comment: "rcv group event chat item"), profile.profileViewName)
}
case let .userRole(role):
return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text)
return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text(isChannel: isChannel))
case let .memberDeleted(_, profile):
return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName)
case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item")
@@ -5650,9 +5662,9 @@ public enum SndGroupEvent: Decodable, Hashable {
func text(isChannel: Bool) -> String {
switch self {
case let .memberRole(_, profile, role):
return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text)
return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text(isChannel: isChannel))
case let .userRole(role):
return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text)
return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text(isChannel: isChannel))
case let .memberBlocked(_, profile, blocked):
if blocked {
return String.localizedStringWithFormat(NSLocalizedString("you blocked %@", comment: "snd group event chat item"), profile.profileViewName)
@@ -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>