ui: knocking UI improvements (texts, icons, layout, chat view shortcut split toolbar, set admission on creating group) (#5879)

This commit is contained in:
spaced4ndy
2025-05-08 13:44:55 +00:00
committed by GitHub
parent f5926e8b84
commit 2a9df3e10b
11 changed files with 221 additions and 140 deletions

View File

@@ -455,7 +455,7 @@ struct ChatView: View {
if let groupMember = groupMember_ {
MemberSupportChatToolbar(groupMember: groupMember)
} else {
textChatToolbar("Support")
textChatToolbar("Chat with admins")
}
}
case let .msgContentTagContext(contentTag):
@@ -502,7 +502,7 @@ struct ChatView: View {
private func customUserSupportChatNavigationBar() -> some View {
VStack(spacing: 0) {
HStack {
Text("Support")
Text("Chat with admins")
.font(.headline)
.foregroundColor(theme.colors.onBackground)
}

View File

@@ -78,6 +78,12 @@ struct AddGroupMembersViewCommon: View {
let count = selectedContacts.count
Section {
if creatingGroup {
MemberAdmissionButton(
groupInfo: $groupInfo,
admission: groupInfo.groupProfile.memberAdmission_,
currentAdmission: groupInfo.groupProfile.memberAdmission_,
creatingGroup: true
)
GroupPreferencesButton(
groupInfo: $groupInfo,
preferences: groupInfo.fullGroupPreferences,

View File

@@ -89,6 +89,18 @@ struct GroupChatInfoView: View {
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section {
if groupInfo.canAddMembers && groupInfo.businessChat == nil {
groupLinkButton()
}
if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator {
memberSupportButton()
}
if groupInfo.canModerate {
GroupReportsChatNavLink(
chat: chat,
im: ItemsModel(secondaryIMFilter: .msgContentTagContext(contentTag: .report))
)
}
if groupInfo.membership.supportChat != nil {
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
UserSupportChatNavLink(
@@ -96,15 +108,8 @@ struct GroupChatInfoView: View {
im: ItemsModel(secondaryIMFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo))
)
}
if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator {
memberSupportButton()
}
if groupInfo.canModerate {
GroupReportsChatNavLink(
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil), chatItems: [], chatStats: ChatStats()),
im: ItemsModel(secondaryIMFilter: .msgContentTagContext(contentTag: .report))
)
}
} header: {
Text("")
}
Section {
@@ -115,8 +120,6 @@ struct GroupChatInfoView: View {
addOrEditWelcomeMessage()
}
GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences)
} header: {
Text("")
} footer: {
let label: LocalizedStringKey = (
groupInfo.businessChat == nil
@@ -145,9 +148,6 @@ struct GroupChatInfoView: View {
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
if groupInfo.canAddMembers {
if groupInfo.businessChat == nil {
groupLinkButton()
}
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
@@ -550,7 +550,7 @@ struct GroupChatInfoView: View {
userSupportChatNavLinkActive = true
}
} label: {
Label("Support chat", systemImage: "flag")
Label("Chat with admins", systemImage: "flag")
}
NavigationLink(isActive: $userSupportChatNavLinkActive) {
@@ -573,16 +573,20 @@ struct GroupChatInfoView: View {
private func memberSupportButton() -> some View {
NavigationLink {
MemberSupportView(groupInfo: groupInfo)
.navigationBarTitle("Member support")
.navigationBarTitle("Chats with members")
.modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large)
} label: {
Label("Member support", systemImage: "flag")
Label(
"Chats with members",
systemImage: chat.chatStats.supportChatsUnreadCount > 0 ? "flag.fill" : "flag"
)
}
}
struct GroupReportsChatNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@State private var groupReportsChatNavLinkActive = false
@ObservedObject var chat: Chat
var im: ItemsModel
@@ -594,7 +598,11 @@ struct GroupChatInfoView: View {
groupReportsChatNavLinkActive = true
}
} label: {
Label("Member reports", systemImage: "flag")
Label(
"Member reports",
systemImage: chat.chatStats.reportsCount > 0 ? "flag.fill" : "flag"
)
.foregroundColor(chat.chatStats.reportsCount > 0 ? .red : theme.colors.primary)
}
NavigationLink(isActive: $groupReportsChatNavLinkActive) {

View File

@@ -33,8 +33,8 @@ struct GroupPreferencesView: View {
Section {
MemberAdmissionButton(
groupInfo: $groupInfo,
admission: groupInfo.groupProfile.memberAdmission ?? GroupMemberAdmission(),
currentAdmission: groupInfo.groupProfile.memberAdmission ?? GroupMemberAdmission(),
admission: groupInfo.groupProfile.memberAdmission_,
currentAdmission: groupInfo.groupProfile.memberAdmission_,
creatingGroup: creatingGroup
)
}
@@ -85,62 +85,6 @@ struct GroupPreferencesView: View {
}
}
struct MemberAdmissionButton: View {
@Binding var groupInfo: GroupInfo
@State var admission: GroupMemberAdmission
@State var currentAdmission: GroupMemberAdmission
var creatingGroup: Bool = false
var body: some View {
NavigationLink {
MemberAdmissionView(
groupInfo: $groupInfo,
admission: $admission,
currentAdmission: currentAdmission,
creatingGroup: creatingGroup,
saveAdmission: saveAdmission
)
.navigationBarTitle("Member admission")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
.onDisappear {
let saveText = NSLocalizedString(
creatingGroup ? "Save" : "Save and notify group members",
comment: "alert button"
)
if groupInfo.groupProfile.memberAdmission != admission {
showAlert(
title: NSLocalizedString("Save admission settings?", comment: "alert title"),
buttonTitle: saveText,
buttonAction: { saveAdmission() },
cancelButton: true
)
}
}
} label: {
Label("Member admission", systemImage: "switch.2")
}
}
private func saveAdmission() {
Task {
do {
var gp = groupInfo.groupProfile
gp.memberAdmission = admission
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
currentAdmission = admission
}
} catch {
logger.error("MemberAdmissionView apiUpdateGroup error: \(responseError(error))")
}
}
}
}
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
Section {
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
@@ -204,6 +148,66 @@ struct GroupPreferencesView: View {
}
}
struct MemberAdmissionButton: View {
@Binding var groupInfo: GroupInfo
@State var admission: GroupMemberAdmission
@State var currentAdmission: GroupMemberAdmission
var creatingGroup: Bool = false
var body: some View {
NavigationLink {
MemberAdmissionView(
groupInfo: $groupInfo,
admission: $admission,
currentAdmission: currentAdmission,
creatingGroup: creatingGroup,
saveAdmission: saveAdmission
)
.navigationBarTitle("Member admission")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
.onDisappear {
let saveText = NSLocalizedString(
creatingGroup ? "Save" : "Save and notify group members",
comment: "alert button"
)
if groupInfo.groupProfile.memberAdmission_ != admission {
showAlert(
title: NSLocalizedString("Save admission settings?", comment: "alert title"),
buttonTitle: saveText,
buttonAction: { saveAdmission() },
cancelButton: true
)
}
}
} label: {
if creatingGroup {
Text("Set member admission")
} else {
Label("Member admission", systemImage: "switch.2")
}
}
}
private func saveAdmission() {
Task {
do {
var gp = groupInfo.groupProfile
gp.memberAdmission = admission
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
currentAdmission = admission
}
} catch {
logger.error("MemberAdmissionView apiUpdateGroup error: \(responseError(error))")
}
}
}
}
struct GroupPreferencesView_Previews: PreviewProvider {
static var previews: some View {
GroupPreferencesView(

View File

@@ -30,8 +30,8 @@ struct MemberAdmissionView: View {
VStack {
List {
admissionSection(
NSLocalizedString("Review", comment: "admission stage"),
NSLocalizedString("Review new members before admitting to group.", comment: "admission stage description"),
NSLocalizedString("Review members", comment: "admission stage"),
NSLocalizedString("Review members before admitting (\"knocking\").", comment: "admission stage description"),
$admission.review
)

View File

@@ -44,7 +44,7 @@ struct MemberSupportView: View {
: membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
if membersWithChats.isEmpty {
Text("No support chats")
Text("No chats with members")
.foregroundColor(.secondary)
} else {
List {

View File

@@ -2101,6 +2101,11 @@ public struct GroupProfile: Codable, NamedChat, Hashable {
public var memberAdmission: GroupMemberAdmission?
public var localAlias: String { "" }
public var memberAdmission_: GroupMemberAdmission {
get { self.memberAdmission ?? GroupMemberAdmission() }
set { memberAdmission = newValue }
}
public static let sampleData = GroupProfile(
displayName: "team",
fullName: "My Team"

View File

@@ -342,7 +342,7 @@ fun ChatView(
}
}
},
showReportsOrSupportChats = {
showReports = {
val info = activeChatInfo.value ?: return@ChatLayout
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
@@ -350,9 +350,18 @@ fun ChatView(
}
hideKeyboard(view)
scope.launch {
if (reportsCount > 0) {
showGroupReportsView(staleChatId, scrollToItemId, info)
} else if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
showGroupReportsView(staleChatId, scrollToItemId, info)
}
},
showSupportChats = {
val info = activeChatInfo.value ?: return@ChatLayout
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
return@ChatLayout
}
hideKeyboard(view)
scope.launch {
if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
ModalManager.end.showCustomModal { close ->
MemberSupportView(
chatRh,
@@ -733,7 +742,8 @@ fun ChatLayout(
selectedChatItems: MutableState<Set<Long>?>,
back: () -> Unit,
info: () -> Unit,
showReportsOrSupportChats: () -> Unit,
showReports: () -> Unit,
showSupportChats: () -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
@@ -887,7 +897,7 @@ fun ChatLayout(
val supportChatsUnreadCount = supportChatsUnreadCount(chatInfo?.id)
if (oneHandUI.value && chatBottomBar.value) {
if (chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportChatsUnreadCount > 0)) {
SupportChatsCountToolbar(reportsCount, supportChatsUnreadCount, withStatusBar = true, showReportsOrSupportChats)
SupportChatsCountToolbar(reportsCount, supportChatsUnreadCount, withStatusBar = true, showReports, showSupportChats)
} else {
StatusBarBackground()
}
@@ -944,7 +954,7 @@ fun ChatLayout(
}
}
if ((reportsCount > 0 || supportChatsUnreadCount > 0) && (!oneHandUI.value || !chatBottomBar.value)) {
SupportChatsCountToolbar(reportsCount, supportChatsUnreadCount, withStatusBar = false, showReportsOrSupportChats)
SupportChatsCountToolbar(reportsCount, supportChatsUnreadCount, withStatusBar = false, showReports, showSupportChats)
}
}
}
@@ -1180,37 +1190,64 @@ private fun SupportChatsCountToolbar(
reportsCount: Int,
supportChatsUnreadCount: Int,
withStatusBar: Boolean,
showReportsOrSupportChats: () -> Unit
showReports: () -> Unit,
showSupportChats: () -> Unit
) {
Box {
val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp
Row(
Modifier
.fillMaxWidth()
.height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
.background(MaterialTheme.colors.background)
.clickable(onClick = showReportsOrSupportChats)
.padding(top = statusBarPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
val iconColor = if (reportsCount == 0) MaterialTheme.colors.primary else MaterialTheme.colors.error
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = iconColor)
Spacer(Modifier.width(4.dp))
Text(
if (supportChatsUnreadCount == 0) {
if (reportsCount == 1) {
stringResource(MR.strings.group_reports_active_one)
} else {
stringResource(MR.strings.group_reports_active).format(reportsCount)
}
} else if (reportsCount == 0) {
stringResource(MR.strings.group_new_support_messages).format(supportChatsUnreadCount)
} else {
String.format(generalGetString(MR.strings.group_reports_active_new_support_messages), reportsCount, supportChatsUnreadCount)
},
style = MaterialTheme.typography.button
)
if (reportsCount > 0) {
Row(
Modifier
.fillMaxWidth()
.weight(1F)
.height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
.background(MaterialTheme.colors.background)
.clickable(onClick = showReports)
.padding(top = statusBarPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
Spacer(Modifier.width(4.dp))
Text(
if (reportsCount == 1) {
stringResource(MR.strings.group_reports_active_one)
} else {
stringResource(MR.strings.group_reports_active).format(reportsCount)
},
style = MaterialTheme.typography.button
)
}
}
if (supportChatsUnreadCount > 0) {
Row(
Modifier
.fillMaxWidth()
.weight(1F)
.height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
.background(MaterialTheme.colors.background)
.clickable(onClick = showSupportChats)
.padding(top = statusBarPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.primary)
Spacer(Modifier.width(4.dp))
Text(
if (appPlatform.isAndroid)
stringResource(MR.strings.group_new_support_messages_short).format(supportChatsUnreadCount)
else
stringResource(MR.strings.group_new_support_messages).format(supportChatsUnreadCount),
style = MaterialTheme.typography.button
)
}
}
}
Divider(Modifier.align(Alignment.BottomStart))
}
@@ -3027,7 +3064,8 @@ fun PreviewChatLayout() {
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
showReportsOrSupportChats = {},
showReports = {},
showSupportChats = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
@@ -3105,7 +3143,8 @@ fun PreviewGroupChatLayout() {
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
showReportsOrSupportChats = {},
showReports = {},
showSupportChats = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },

View File

@@ -55,6 +55,16 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
GroupPreferencesView(chatModel, rhId, groupInfo.id, close)
}
},
openMemberAdmission = {
ModalManager.end.showCustomModal { close ->
MemberAdmissionView(
chat.simplex.common.platform.chatModel,
rhId,
groupInfo.id,
close
)
}
},
inviteMembers = {
allowModifyMembers = false
withLongRunningApi(slow = 120_000) {
@@ -110,6 +120,7 @@ fun AddGroupMembersLayout(
allowModifyMembers: Boolean,
searchText: MutableState<TextFieldValue>,
openPreferences: () -> Unit,
openMemberAdmission: () -> Unit,
inviteMembers: () -> Unit,
clearSelection: () -> Unit,
addContact: (Long) -> Unit,
@@ -165,6 +176,9 @@ fun AddGroupMembersLayout(
} else {
SectionView {
if (creatingGroup) {
SectionItemView(openMemberAdmission) {
Text(stringResource(MR.strings.set_member_admission))
}
SectionItemView(openPreferences) {
Text(stringResource(MR.strings.set_group_preferences))
}
@@ -376,6 +390,7 @@ fun PreviewAddGroupMembersLayout() {
allowModifyMembers = true,
searchText = remember { mutableStateOf(TextFieldValue("")) },
openPreferences = {},
openMemberAdmission = {},
inviteMembers = {},
clearSelection = {},
addContact = {},

View File

@@ -463,22 +463,30 @@ fun ModalData.GroupChatInfoLayout(
var anyTopSectionRowShow = false
SectionView {
if (groupInfo.membership.supportChat != null) {
if (groupInfo.canAddMembers && groupInfo.businessChat == null) {
anyTopSectionRowShow = true
UserSupportChatButton(groupInfo, scrollToItemId)
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
}
}
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
anyTopSectionRowShow = true
MemberSupportButton(openMemberSupport)
MemberSupportButton(chat, openMemberSupport)
}
if (groupInfo.canModerate) {
anyTopSectionRowShow = true
GroupReportsButton {
GroupReportsButton(chat) {
scope.launch {
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
}
}
}
if (groupInfo.membership.supportChat != null) {
anyTopSectionRowShow = true
UserSupportChatButton(groupInfo, scrollToItemId)
}
}
if (anyTopSectionRowShow) {
SectionDividerSpaced(maxBottomPadding = false)
@@ -520,13 +528,6 @@ fun ModalData.GroupChatInfoLayout(
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
if (groupInfo.canAddMembers) {
if (groupInfo.businessChat == null) {
if (groupLink == null) {
CreateGroupLinkButton(manageGroupLink)
} else {
GroupLinkButton(manageGroupLink)
}
}
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
@@ -738,11 +739,12 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
}
@Composable
private fun MemberSupportButton(onClick: () -> Unit) {
private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_flag),
painterResource(if (chat.chatStats.supportChatsUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
stringResource(MR.strings.member_support),
click = onClick
click = onClick,
iconColor = (if (chat.chatStats.supportChatsUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)
)
}
@@ -756,11 +758,12 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit)
}
@Composable
private fun GroupReportsButton(onClick: () -> Unit) {
private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_flag),
painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
stringResource(MR.strings.group_reports_member_reports),
click = onClick
click = onClick,
iconColor = (if (chat.chatStats.reportsCount > 0) Color.Red else MaterialTheme.colors.secondary)
)
}

View File

@@ -466,7 +466,7 @@
<string name="group_reports_active">%d reports</string>
<string name="group_reports_member_reports">Member reports</string>
<string name="group_new_support_messages">%d new support messages</string>
<string name="group_reports_active_new_support_messages">%1$d reports, %2$d new support messages</string>
<string name="group_new_support_messages_short">%d messages</string>
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>
@@ -512,7 +512,7 @@
<string name="report_compose_reason_header_illegal">Report content: only group moderators will see it.</string>
<string name="report_compose_reason_header_other">Report other: only group moderators will see it.</string>
<string name="report_sent_alert_title">Report sent to moderators</string>
<string name="report_sent_alert_msg_view_in_support_chat">You can view your reports in Support Chat.</string>
<string name="report_sent_alert_msg_view_in_support_chat">You can view your reports in Chat with admins.</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
@@ -1767,7 +1767,7 @@
<string name="button_remove_member_question">Remove member?</string>
<string name="button_remove_members_question">Remove members?</string>
<string name="button_remove_member">Remove member</string>
<string name="button_support_chat">Support chat</string>
<string name="button_support_chat">Chat with admins</string>
<string name="button_send_direct_message">Send direct message</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
<string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string>
@@ -2044,6 +2044,7 @@
<string name="contact_preferences">Contact preferences</string>
<string name="group_preferences">Group preferences</string>
<string name="set_group_preferences">Set group preferences</string>
<string name="set_member_admission">Set member admission</string>
<string name="your_preferences">Your preferences</string>
<string name="timed_messages">Disappearing messages</string>
<string name="direct_messages">Direct messages</string>
@@ -2163,17 +2164,17 @@
<!-- MemberAdmission.kt -->
<string name="member_admission">Member admission</string>
<string name="admission_stage_review">Review</string>
<string name="admission_stage_review_descr">Review new members before admitting to group.</string>
<string name="admission_stage_review">Review members</string>
<string name="admission_stage_review_descr">Review members before admitting ("knocking").</string>
<string name="member_criteria_off">off</string>
<string name="member_criteria_all">all</string>
<!-- MemberSupportView.kt -->
<string name="member_support">Member support</string>
<string name="no_support_chats">No support chats</string>
<string name="member_support">Chats with members</string>
<string name="no_support_chats">No chats with members</string>
<!-- MemberSupportChatView.kt -->
<string name="support_chat">Support</string>
<string name="support_chat">Chat with admins</string>
<string name="remove_pending_member_button">Remove</string>
<string name="accept_pending_member_button">Accept</string>
<string name="accept_pending_member_alert_title">Accept member</string>