From aa7e377bce6e369c0cbbf53859ee4e5f2dde0756 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Wed, 27 Jul 2022 11:16:07 +0400 Subject: [PATCH] ios: groups miscellaneous (#843) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/SimpleXAPI.swift | 6 +- .../Shared/Views/Chat/ChatInfoToolbar.swift | 2 +- apps/ios/Shared/Views/Chat/ChatView.swift | 39 +++--- .../Chat/Group/AddGroupMembersView.swift | 113 +++++++++++------- .../Chat/Group/GroupMemberInfoView.swift | 74 +++++++++--- .../Shared/Views/Chat/GroupChatInfoView.swift | 102 ++++++++++------ .../Shared/Views/NewChat/AddGroupView.swift | 8 +- .../Shared/Views/NewChat/NewChatButton.swift | 2 +- apps/ios/SimpleXChat/APITypes.swift | 4 +- apps/ios/SimpleXChat/ChatTypes.swift | 52 +++++--- src/Simplex/Chat.hs | 3 +- tests/ChatTests.hs | 4 +- 12 files changed, 265 insertions(+), 144 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index bdedb5072..7274839ca 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -306,7 +306,7 @@ func apiContactInfo(contactId: Int64) async throws -> ConnectionStats? { throw r } -func apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) async throws -> ConnectionStats? { +func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> ConnectionStats? { let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId)) if case let .groupMemberInfo(_, _, connStats_) = r { return connStats_ } throw r @@ -565,9 +565,9 @@ func apiNewGroup(_ gp: GroupProfile) throws -> GroupInfo { throw r } -func addMember(groupId: Int64, contactId: Int64) async { +func addMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async { do { - try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: .admin) + try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole) } catch let error { logger.error("addMember error: \(responseError(error))") } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index a5fe3b61d..d57e0b992 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -10,7 +10,7 @@ import SwiftUI import SimpleXChat let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) -let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 ) +let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2) struct ChatInfoToolbar: View { @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 9b45a05a2..19849a4a9 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -11,19 +11,12 @@ import SimpleXChat private let memberImageSize: CGFloat = 34 -enum ChatViewSheet: Identifiable { - case chatInfo - case addMember - - var id: ChatViewSheet { get { self } } -} - struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat - @State private var showChatViewSheet: Bool = false - @State private var chatViewSheet: ChatViewSheet? + @State private var showChatInfoSheet: Bool = false + @State private var showAddMembersSheet: Bool = false @State private var composeState = ComposeState() @State private var deletingItem: ChatItem? = nil @FocusState private var keyboardVisible: Bool @@ -107,22 +100,15 @@ struct ChatView: View { } ToolbarItem(placement: .principal) { Button { - chatViewSheet = .chatInfo - showChatViewSheet = true + showChatInfoSheet = true } label: { ChatInfoToolbar(chat: chat) } - .sheet(isPresented: $showChatViewSheet) { - switch chatViewSheet { - case .chatInfo: - if case .direct = chat.chatInfo { - ChatInfoView(chat: chat, showSheet: $showChatViewSheet) - } else if case let .group(groupInfo) = chat.chatInfo { - GroupChatInfoView(chat: chat, groupInfo: groupInfo, showSheet: $showChatViewSheet) - } - case .addMember: - AddGroupMembersView(chat: chat, showSheet: $showChatViewSheet) - default: EmptyView() + .sheet(isPresented: $showChatInfoSheet) { + if case .direct = chat.chatInfo { + ChatInfoView(chat: chat, showSheet: $showChatInfoSheet) + } else if case let .group(groupInfo) = chat.chatInfo { + GroupChatInfoView(chat: chat, groupInfo: groupInfo, showSheet: $showChatInfoSheet) } } } @@ -132,8 +118,12 @@ struct ChatView: View { callButton(contact, .audio, imageName: "phone") callButton(contact, .video, imageName: "video") } - } else if case .group = chat.chatInfo { + } else if case let .group(groupInfo) = chat.chatInfo, + groupInfo.canAddMembers { addMembersButton() + .sheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet) + } } } } @@ -150,8 +140,7 @@ struct ChatView: View { private func addMembersButton() -> some View { Button { - chatViewSheet = .addMember - showChatViewSheet = true + showAddMembersSheet = true } label: { Image(systemName: "person.crop.circle.badge.plus") } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 55b996c23..ebc62a9ce 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -12,56 +12,53 @@ import SimpleXChat struct AddGroupMembersView: View { @EnvironmentObject var chatModel: ChatModel var chat: Chat + var groupInfo: GroupInfo @Binding var showSheet: Bool @State private var contactsToAdd: [Contact] = [] @State private var selectedContacts = Set() + @State private var selectedRole: GroupMemberRole = .admin var body: some View { - VStack(alignment: .leading, spacing: 0) { - ChatInfoToolbar(chat: chat, imageSize: 48) - .padding() - .frame(maxWidth: .infinity, alignment: .center) - .background(Color(uiColor: .quaternarySystemFill)) - if (contactsToAdd.isEmpty) { - Text("No contacts to add") - .foregroundColor(.secondary) - .padding() - .frame(maxWidth: .infinity, alignment: .center) - } else { - HStack { - let count = selectedContacts.count - Button { - Task { - for contactId in selectedContacts { - await addMember(groupId: chat.chatInfo.apiId, contactId: contactId) - } - showSheet = false - } - } label: { - Label( - count > 0 ? "Invite \(count) member(s)" : "Invite new members", - systemImage: "plus") - } - .disabled(count < 1) - Spacer() - if count > 0 { - Button { - selectedContacts.removeAll() - } label: { - Label("Clear", systemImage: "multiply") - } - } - } - .padding(.horizontal) - .padding(.bottom, 12) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(uiColor: .quaternarySystemFill)) - List(contactsToAdd) { contact in - contactCheckView(contact) + NavigationView { + List { + ChatInfoToolbar(chat: chat, imageSize: 48) + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + + if (contactsToAdd.isEmpty) { + Text("No contacts to add") + .foregroundColor(.secondary) + .padding() + .frame(maxWidth: .infinity, alignment: .center) .listRowBackground(Color.clear) + } else { + let count = selectedContacts.count + Section { + rolePicker() + inviteMembersButton() + .disabled(count < 1) + } footer: { + if (count >= 1) { + HStack { + Button { selectedContacts.removeAll() } label: { Text("Clear") } + Spacer() + Text("\(count) contact(s) selected") + } + } else { + Text("No contacts selected") + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + + Section { + ForEach(contactsToAdd) { contact in + contactCheckView(contact) + } + } } - .listStyle(.plain) } + .navigationBarHidden(true) } .frame(maxHeight: .infinity, alignment: .top) .task { @@ -78,6 +75,33 @@ struct AddGroupMembersView: View { .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } + func inviteMembersButton() -> some View { + Button { + Task { + for contactId in selectedContacts { + await addMember(groupId: chat.chatInfo.apiId, contactId: contactId, memberRole: selectedRole) + } + showSheet = false + } + } label: { + HStack { + Text("Invite to group") + Image(systemName: "checkmark") + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + + func rolePicker() -> some View { + Picker("New member role", selection: $selectedRole) { + ForEach(GroupMemberRole.allCases) { role in + if role <= groupInfo.membership.memberRole { + Text(role.text) + } + } + } + } + func contactCheckView(_ contact: Contact) -> some View { let checked = selectedContacts.contains(contact.apiId) return Button { @@ -92,10 +116,11 @@ struct AddGroupMembersView: View { .frame(width: 30, height: 30) .padding(.trailing, 2) Text(ChatInfo.direct(contact: contact).chatViewName) + .foregroundColor(.primary) .lineLimit(1) Spacer() Image(systemName: checked ? "checkmark.circle.fill": "circle") - .foregroundColor(checked ? .accentColor : .secondary) + .foregroundColor(checked ? .accentColor : Color(uiColor: .tertiaryLabel)) } } } @@ -104,6 +129,6 @@ struct AddGroupMembersView: View { struct AddGroupMembersView_Previews: PreviewProvider { static var previews: some View { @State var showSheet = true - return AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), showSheet: $showSheet) + return AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData, showSheet: $showSheet) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index c9c413151..75f1f5794 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -11,8 +11,11 @@ import SimpleXChat struct GroupMemberInfoView: View { @EnvironmentObject var chatModel: ChatModel + @Environment(\.dismiss) var dismiss: DismissAction + var groupInfo: GroupInfo var member: GroupMember - @State private var alert: GroupMemberInfoViewAlert? = nil + @State private var alert: GroupMemberInfoViewAlert? + @State private var connectionStats: ConnectionStats? enum GroupMemberInfoViewAlert: Identifiable { case removeMemberAlert @@ -26,20 +29,34 @@ struct GroupMemberInfoView: View { groupMemberInfoHeader() .listRowBackground(Color.clear) - // TODO server status - - Section(header: Text("Info")) { - Text("Role: ") + Text(member.memberRole.text) + Section("Member") { + infoRow("Group", groupInfo.displayName) + // TODO change role + // localizedInfoRow("Role", member.memberRole.text) // TODO invited by - need to get contact by contact id - Text("Status: ") + Text(member.memberStatus.text) if let conn = member.activeConn { - let connLevelDesc = conn.connLevel == 0 ? "Direct" : "Indirect (\(conn.connLevel))" - Text("Connection level: \(connLevelDesc)") + let connLevelDesc = conn.connLevel == 0 ? "direct" : "indirect (\(conn.connLevel))" + infoRow("Connection", connLevelDesc) + } + } + + if let connStats = connectionStats { + Section("Servers") { + // TODO network connection status + smpServers("receiving via", connStats.rcvServers) + smpServers("sending via", connStats.sndServers) } } Section { - removeMemberButton() + if member.canRemove(userRole: groupInfo.membership.memberRole) && member.memberStatus != .memRemoved { + removeMemberButton() + } + } + + Section("For console") { + infoRow("Local name", member.localDisplayName) + infoRow("Database ID", "\(member.groupMemberId)") } } .navigationBarHidden(true) @@ -50,25 +67,50 @@ struct GroupMemberInfoView: View { case .removeMemberAlert: return removeMemberAlert() } } + .task { + do { + let stats = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + await MainActor.run { connectionStats = stats } + } catch let error { + logger.error("apiGroupMemberInfo error: \(responseError(error))") + } + } } - func groupMemberInfoHeader() -> some View { + private func groupMemberInfoHeader() -> some View { VStack { ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill)) .frame(width: 192, height: 192) .padding(.top, 12) .padding() - Text(member.localDisplayName) + Text(member.displayName) .font(.largeTitle) .lineLimit(1) .padding(.bottom, 2) - Text(member.fullName) - .font(.title2) - .lineLimit(2) + if member.fullName != "" && member.fullName != member.displayName { + Text(member.fullName) + .font(.title2) + .lineLimit(2) + } } .frame(maxWidth: .infinity, alignment: .center) } + @ViewBuilder private func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View { + if let servers = servers, + servers.count > 0 { + infoRow(title, serverHost(servers[0])) + } + } + + private func serverHost(_ s: String) -> String { + if let i = s.range(of: "@")?.lowerBound { + return String(s[i...].dropFirst()) + } else { + return s + } + } + func removeMemberButton() -> some View { Button(role: .destructive) { alert = .removeMemberAlert @@ -86,7 +128,7 @@ struct GroupMemberInfoView: View { Task { do { _ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId) - // TODO navigate back + dismiss() } catch let error { logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)") } @@ -99,6 +141,6 @@ struct GroupMemberInfoView: View { struct GroupMemberInfoView_Previews: PreviewProvider { static var previews: some View { - GroupMemberInfoView(member: GroupMember.sampleData) + return GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData) } } diff --git a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift index a99b18915..f11d2506d 100644 --- a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift @@ -9,6 +9,24 @@ import SwiftUI import SimpleXChat +func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundStyle(.secondary) + } +} + +func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey) -> some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundStyle(.secondary) + } +} + struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @@ -18,6 +36,7 @@ struct GroupChatInfoView: View { @State private var members: [GroupMember] = [] @State private var alert: GroupChatInfoViewAlert? = nil @State private var showAddMembersSheet: Bool = false + @State private var selectedMember: GroupMember? = nil enum GroupChatInfoViewAlert: Identifiable { case deleteGroupAlert @@ -33,11 +52,20 @@ struct GroupChatInfoView: View { groupInfoHeader() .listRowBackground(Color.clear) - Section(header: Text("\(members.count) Members")) { - addMembersButton() - ForEach(members) { member in - memberView(member) + Section(header: Text("\(members.count + 1) members")) { + if (groupInfo.canAddMembers) { + addMembersButton() } + memberView(groupInfo.membership, user: true) + ForEach(members) { member in + Button { selectedMember = member } label: { memberView(member) } + } + } + .sheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet) + } + .sheet(item: $selectedMember) { member in + GroupMemberInfoView(groupInfo: groupInfo, member: member) } Section { @@ -45,15 +73,19 @@ struct GroupChatInfoView: View { if groupInfo.canDelete { deleteGroupButton() } - leaveGroupButton() + if (groupInfo.membership.memberStatus != .memLeft) { + leaveGroupButton() + } + } + + Section(header: Text("For console")) { + infoRow("Local name", chat.chatInfo.localDisplayName) + infoRow("Database ID", "\(chat.chatInfo.apiId)") } } .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, showSheet: $showAddMembersSheet) - } .alert(item: $alert) { alertItem in switch(alertItem) { case .deleteGroupAlert: return deleteGroupAlert() @@ -69,17 +101,20 @@ struct GroupChatInfoView: View { func groupInfoHeader() -> some View { VStack { + let cInfo = chat.chatInfo ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) .frame(width: 192, height: 192) .padding(.top, 12) .padding() - Text(chat.chatInfo.localDisplayName) + Text(cInfo.displayName) .font(.largeTitle) .lineLimit(1) .padding(.bottom, 2) - Text(chat.chatInfo.fullName) - .font(.title2) - .lineLimit(2) + if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName { + Text(cInfo.fullName) + .font(.title2) + .lineLimit(2) + } } .frame(maxWidth: .infinity, alignment: .center) } @@ -98,28 +133,27 @@ struct GroupChatInfoView: View { .foregroundColor(status == .connected ? .green : .secondary) } - func memberView(_ member: GroupMember) -> some View { - NavigationLink { - GroupMemberInfoView(member: member) - } label: { - HStack{ - ProfileImage(imageStr: member.image) - .frame(width: 38, height: 38) - .padding(.trailing, 2) - // TODO server connection status - VStack(alignment: .leading) { - Text(member.chatViewName) - .lineLimit(1) - Text(member.memberStatus.shortText) - .lineLimit(1) - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - let role = member.memberRole - if role == .owner || role == .admin { - Text(member.memberRole.text) - } + func memberView(_ member: GroupMember, user: Bool = false) -> some View { + HStack{ + ProfileImage(imageStr: member.image) + .frame(width: 38, height: 38) + .padding(.trailing, 2) + // TODO server connection status + VStack(alignment: .leading) { + Text(member.chatViewName) + .lineLimit(1) + .foregroundColor(.primary) + let s = Text(member.memberStatus.shortText) + (user ? Text ("you: ") + s : s) + .lineLimit(1) + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + let role = member.memberRole + if role == .owner || role == .admin { + Text(member.memberRole.text) + .foregroundColor(.secondary) } } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 5117cfc3a..92a9de395 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -85,7 +85,13 @@ struct AddGroupView: View { m.chatId = groupInfo.id } } catch { - fatalError("Failed to create group: \(responseError(error))") + openedSheet = nil + AlertManager.shared.showAlert( + Alert( + title: Text("Failed to create group"), + message: Text(responseError(error)) + ) + ) } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index c2e662409..19a88ca3c 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -28,7 +28,7 @@ struct NewChatButton: View { Image(systemName: "plus.circle.fill") .resizable() .scaledToFit() - .frame(width: 24, height: 24 ) + .frame(width: 24, height: 24) } .confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) { Button("Create link / QR code") { addContactAction() } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 4f46438da..6d96baf17 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -500,8 +500,8 @@ public struct NetCfg: Codable { } public struct ConnectionStats: Codable { - var rcvServers: [String]? - var sndServers: [String]? + public var rcvServers: [String]? + public var sndServers: [String]? } public protocol SelectableItem: Hashable, Identifiable { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index ee8778d2f..9cea750a8 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -444,6 +444,10 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { return membership.memberRole == .owner || (s == .memRemoved || s == .memLeft || s == .memGroupDeleted || s == .memInvited) } + public var canAddMembers: Bool { + return membership.memberRole >= .admin && membership.memberActive + } + public static let sampleData = GroupInfo( groupId: 1, localDisplayName: "team", @@ -524,6 +528,10 @@ public struct GroupMember: Identifiable, Decodable { } } + public func canRemove(userRole: GroupMemberRole) -> Bool { + return userRole >= .admin && userRole >= memberRole + } + public static let sampleData = GroupMember( groupMemberId: 1, groupId: 1, @@ -539,18 +547,32 @@ public struct GroupMember: Identifiable, Decodable { ) } -public enum GroupMemberRole: String, Decodable { +public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { case member = "member" case admin = "admin" case owner = "owner" + public var id: Self { self } + public var text: LocalizedStringKey { switch self { - case .member: return "Member" - case .admin: return "Admin" - case .owner: return "Owner" + case .member: return "member" + case .admin: return "admin" + case .owner: return "owner" } } + + private var comparisonValue: Int { + switch self { + case .member: return 0 + case .admin: return 1 + case .owner: return 2 + } + } + + public static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.comparisonValue < rhs.comparisonValue + } } public enum GroupMemberCategory: String, Decodable { @@ -576,17 +598,17 @@ public enum GroupMemberStatus: String, Decodable { public var text: LocalizedStringKey { switch self { - case .memRemoved: return "Removed" - case .memLeft: return "Left" - case .memGroupDeleted: return "Group deleted" - case .memInvited: return "Invited" - case .memIntroduced: return "Connecting (introduced)" - case .memIntroInvited: return "Connecting (introduction invitation)" - case .memAccepted: return "Connecting (accepted)" - case .memAnnounced: return "Connecting (announced)" - case .memConnected: return "Connected" - case .memComplete: return "Complete" - case .memCreator: return "Creator" + case .memRemoved: return "removed" + case .memLeft: return "left" + case .memGroupDeleted: return "group deleted" + case .memInvited: return "invited" + case .memIntroduced: return "connecting (introduced)" + case .memIntroInvited: return "connecting (introduction invitation)" + case .memAccepted: return "connecting (accepted)" + case .memAnnounced: return "connecting (announced)" + case .memConnected: return "connected" + case .memComplete: return "complete" + case .memCreator: return "creator" } } diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e2a2b8e8f..18ec7377a 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -745,7 +745,8 @@ processChatCommand = \case Nothing -> throwChatError CEGroupMemberNotFound Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do let userRole = memberRole (membership :: GroupMember) - when (userRole < GRAdmin || userRole < mRole) $ throwChatError CEGroupUserRole + canRemove = userRole >= GRAdmin && userRole >= mRole + unless canRemove $ throwChatError CEGroupUserRole withChatLock . procCmd $ do when (mStatus /= GSMemInvited) $ do msg <- sendGroupMessage gInfo members $ XGrpMemDel mId diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index dc86b962a..f575bd1bb 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -1102,6 +1102,7 @@ testGroupAsync = withTmpFiles $ do cath <## "#team: connected to server(s)" cath <## "#team: member bob (Bob) is connected" ] + threadDelay 500000 print (3 :: Integer) withTestChat "bob" $ \bob -> do withNewTestChat "dan" danProfile $ \dan -> do @@ -1120,7 +1121,8 @@ testGroupAsync = withTmpFiles $ do [ bob <## "#team: dan joined the group", dan <## "#team: you joined the group" ] - threadDelay 500000 + threadDelay 1000000 + threadDelay 500000 print (4 :: Integer) withTestChat "alice" $ \alice -> do withTestChat "cath" $ \cath -> do