ios: groups miscellaneous (#843)

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
JRoberts
2022-07-27 11:16:07 +04:00
committed by GitHub
parent a4aaf36774
commit aa7e377bce
12 changed files with 265 additions and 144 deletions

View File

@@ -306,7 +306,7 @@ func apiContactInfo(contactId: Int64) async throws -> ConnectionStats? {
throw r 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)) let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, connStats_) = r { return connStats_ } if case let .groupMemberInfo(_, _, connStats_) = r { return connStats_ }
throw r throw r
@@ -565,9 +565,9 @@ func apiNewGroup(_ gp: GroupProfile) throws -> GroupInfo {
throw r throw r
} }
func addMember(groupId: Int64, contactId: Int64) async { func addMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async {
do { do {
try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: .admin) try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)
} catch let error { } catch let error {
logger.error("addMember error: \(responseError(error))") logger.error("addMember error: \(responseError(error))")
} }

View File

@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat import SimpleXChat
let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) 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 { struct ChatInfoToolbar: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat @ObservedObject var chat: Chat

View File

@@ -11,19 +11,12 @@ import SimpleXChat
private let memberImageSize: CGFloat = 34 private let memberImageSize: CGFloat = 34
enum ChatViewSheet: Identifiable {
case chatInfo
case addMember
var id: ChatViewSheet { get { self } }
}
struct ChatView: View { struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ObservedObject var chat: Chat @ObservedObject var chat: Chat
@State private var showChatViewSheet: Bool = false @State private var showChatInfoSheet: Bool = false
@State private var chatViewSheet: ChatViewSheet? @State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState() @State private var composeState = ComposeState()
@State private var deletingItem: ChatItem? = nil @State private var deletingItem: ChatItem? = nil
@FocusState private var keyboardVisible: Bool @FocusState private var keyboardVisible: Bool
@@ -107,22 +100,15 @@ struct ChatView: View {
} }
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
Button { Button {
chatViewSheet = .chatInfo showChatInfoSheet = true
showChatViewSheet = true
} label: { } label: {
ChatInfoToolbar(chat: chat) ChatInfoToolbar(chat: chat)
} }
.sheet(isPresented: $showChatViewSheet) { .sheet(isPresented: $showChatInfoSheet) {
switch chatViewSheet { if case .direct = chat.chatInfo {
case .chatInfo: ChatInfoView(chat: chat, showSheet: $showChatInfoSheet)
if case .direct = chat.chatInfo { } else if case let .group(groupInfo) = chat.chatInfo {
ChatInfoView(chat: chat, showSheet: $showChatViewSheet) GroupChatInfoView(chat: chat, groupInfo: groupInfo, showSheet: $showChatInfoSheet)
} else if case let .group(groupInfo) = chat.chatInfo {
GroupChatInfoView(chat: chat, groupInfo: groupInfo, showSheet: $showChatViewSheet)
}
case .addMember:
AddGroupMembersView(chat: chat, showSheet: $showChatViewSheet)
default: EmptyView()
} }
} }
} }
@@ -132,8 +118,12 @@ struct ChatView: View {
callButton(contact, .audio, imageName: "phone") callButton(contact, .audio, imageName: "phone")
callButton(contact, .video, imageName: "video") callButton(contact, .video, imageName: "video")
} }
} else if case .group = chat.chatInfo { } else if case let .group(groupInfo) = chat.chatInfo,
groupInfo.canAddMembers {
addMembersButton() addMembersButton()
.sheet(isPresented: $showAddMembersSheet) {
AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet)
}
} }
} }
} }
@@ -150,8 +140,7 @@ struct ChatView: View {
private func addMembersButton() -> some View { private func addMembersButton() -> some View {
Button { Button {
chatViewSheet = .addMember showAddMembersSheet = true
showChatViewSheet = true
} label: { } label: {
Image(systemName: "person.crop.circle.badge.plus") Image(systemName: "person.crop.circle.badge.plus")
} }

View File

@@ -12,56 +12,53 @@ import SimpleXChat
struct AddGroupMembersView: View { struct AddGroupMembersView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
var chat: Chat var chat: Chat
var groupInfo: GroupInfo
@Binding var showSheet: Bool @Binding var showSheet: Bool
@State private var contactsToAdd: [Contact] = [] @State private var contactsToAdd: [Contact] = []
@State private var selectedContacts = Set<Int64>() @State private var selectedContacts = Set<Int64>()
@State private var selectedRole: GroupMemberRole = .admin
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { NavigationView {
ChatInfoToolbar(chat: chat, imageSize: 48) List {
.padding() ChatInfoToolbar(chat: chat, imageSize: 48)
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
.background(Color(uiColor: .quaternarySystemFill)) .listRowBackground(Color.clear)
if (contactsToAdd.isEmpty) { .listRowSeparator(.hidden)
Text("No contacts to add")
.foregroundColor(.secondary) if (contactsToAdd.isEmpty) {
.padding() Text("No contacts to add")
.frame(maxWidth: .infinity, alignment: .center) .foregroundColor(.secondary)
} else { .padding()
HStack { .frame(maxWidth: .infinity, alignment: .center)
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)
.listRowBackground(Color.clear) .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) .frame(maxHeight: .infinity, alignment: .top)
.task { .task {
@@ -78,6 +75,33 @@ struct AddGroupMembersView: View {
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } .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 { func contactCheckView(_ contact: Contact) -> some View {
let checked = selectedContacts.contains(contact.apiId) let checked = selectedContacts.contains(contact.apiId)
return Button { return Button {
@@ -92,10 +116,11 @@ struct AddGroupMembersView: View {
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.padding(.trailing, 2) .padding(.trailing, 2)
Text(ChatInfo.direct(contact: contact).chatViewName) Text(ChatInfo.direct(contact: contact).chatViewName)
.foregroundColor(.primary)
.lineLimit(1) .lineLimit(1)
Spacer() Spacer()
Image(systemName: checked ? "checkmark.circle.fill": "circle") 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 { struct AddGroupMembersView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
@State var showSheet = true @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)
} }
} }

View File

@@ -11,8 +11,11 @@ import SimpleXChat
struct GroupMemberInfoView: View { struct GroupMemberInfoView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
var groupInfo: GroupInfo
var member: GroupMember var member: GroupMember
@State private var alert: GroupMemberInfoViewAlert? = nil @State private var alert: GroupMemberInfoViewAlert?
@State private var connectionStats: ConnectionStats?
enum GroupMemberInfoViewAlert: Identifiable { enum GroupMemberInfoViewAlert: Identifiable {
case removeMemberAlert case removeMemberAlert
@@ -26,20 +29,34 @@ struct GroupMemberInfoView: View {
groupMemberInfoHeader() groupMemberInfoHeader()
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
// TODO server status Section("Member") {
infoRow("Group", groupInfo.displayName)
Section(header: Text("Info")) { // TODO change role
Text("Role: ") + Text(member.memberRole.text) // localizedInfoRow("Role", member.memberRole.text)
// TODO invited by - need to get contact by contact id // TODO invited by - need to get contact by contact id
Text("Status: ") + Text(member.memberStatus.text)
if let conn = member.activeConn { if let conn = member.activeConn {
let connLevelDesc = conn.connLevel == 0 ? "Direct" : "Indirect (\(conn.connLevel))" let connLevelDesc = conn.connLevel == 0 ? "direct" : "indirect (\(conn.connLevel))"
Text("Connection level: \(connLevelDesc)") infoRow("Connection", connLevelDesc)
}
}
if let connStats = connectionStats {
Section("Servers") {
// TODO network connection status
smpServers("receiving via", connStats.rcvServers)
smpServers("sending via", connStats.sndServers)
} }
} }
Section { 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) .navigationBarHidden(true)
@@ -50,25 +67,50 @@ struct GroupMemberInfoView: View {
case .removeMemberAlert: return removeMemberAlert() 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 { VStack {
ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill)) ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill))
.frame(width: 192, height: 192) .frame(width: 192, height: 192)
.padding(.top, 12) .padding(.top, 12)
.padding() .padding()
Text(member.localDisplayName) Text(member.displayName)
.font(.largeTitle) .font(.largeTitle)
.lineLimit(1) .lineLimit(1)
.padding(.bottom, 2) .padding(.bottom, 2)
Text(member.fullName) if member.fullName != "" && member.fullName != member.displayName {
.font(.title2) Text(member.fullName)
.lineLimit(2) .font(.title2)
.lineLimit(2)
}
} }
.frame(maxWidth: .infinity, alignment: .center) .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 { func removeMemberButton() -> some View {
Button(role: .destructive) { Button(role: .destructive) {
alert = .removeMemberAlert alert = .removeMemberAlert
@@ -86,7 +128,7 @@ struct GroupMemberInfoView: View {
Task { Task {
do { do {
_ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId) _ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId)
// TODO navigate back dismiss()
} catch let error { } catch let error {
logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)") logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)")
} }
@@ -99,6 +141,6 @@ struct GroupMemberInfoView: View {
struct GroupMemberInfoView_Previews: PreviewProvider { struct GroupMemberInfoView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
GroupMemberInfoView(member: GroupMember.sampleData) return GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData)
} }
} }

View File

@@ -9,6 +9,24 @@
import SwiftUI import SwiftUI
import SimpleXChat 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 { struct GroupChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared @ObservedObject var alertManager = AlertManager.shared
@@ -18,6 +36,7 @@ struct GroupChatInfoView: View {
@State private var members: [GroupMember] = [] @State private var members: [GroupMember] = []
@State private var alert: GroupChatInfoViewAlert? = nil @State private var alert: GroupChatInfoViewAlert? = nil
@State private var showAddMembersSheet: Bool = false @State private var showAddMembersSheet: Bool = false
@State private var selectedMember: GroupMember? = nil
enum GroupChatInfoViewAlert: Identifiable { enum GroupChatInfoViewAlert: Identifiable {
case deleteGroupAlert case deleteGroupAlert
@@ -33,11 +52,20 @@ struct GroupChatInfoView: View {
groupInfoHeader() groupInfoHeader()
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
Section(header: Text("\(members.count) Members")) { Section(header: Text("\(members.count + 1) members")) {
addMembersButton() if (groupInfo.canAddMembers) {
ForEach(members) { member in addMembersButton()
memberView(member)
} }
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 { Section {
@@ -45,15 +73,19 @@ struct GroupChatInfoView: View {
if groupInfo.canDelete { if groupInfo.canDelete {
deleteGroupButton() 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) .navigationBarHidden(true)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.sheet(isPresented: $showAddMembersSheet) {
AddGroupMembersView(chat: chat, showSheet: $showAddMembersSheet)
}
.alert(item: $alert) { alertItem in .alert(item: $alert) { alertItem in
switch(alertItem) { switch(alertItem) {
case .deleteGroupAlert: return deleteGroupAlert() case .deleteGroupAlert: return deleteGroupAlert()
@@ -69,17 +101,20 @@ struct GroupChatInfoView: View {
func groupInfoHeader() -> some View { func groupInfoHeader() -> some View {
VStack { VStack {
let cInfo = chat.chatInfo
ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill))
.frame(width: 192, height: 192) .frame(width: 192, height: 192)
.padding(.top, 12) .padding(.top, 12)
.padding() .padding()
Text(chat.chatInfo.localDisplayName) Text(cInfo.displayName)
.font(.largeTitle) .font(.largeTitle)
.lineLimit(1) .lineLimit(1)
.padding(.bottom, 2) .padding(.bottom, 2)
Text(chat.chatInfo.fullName) if cInfo.fullName != "" && cInfo.fullName != cInfo.displayName {
.font(.title2) Text(cInfo.fullName)
.lineLimit(2) .font(.title2)
.lineLimit(2)
}
} }
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
} }
@@ -98,28 +133,27 @@ struct GroupChatInfoView: View {
.foregroundColor(status == .connected ? .green : .secondary) .foregroundColor(status == .connected ? .green : .secondary)
} }
func memberView(_ member: GroupMember) -> some View { func memberView(_ member: GroupMember, user: Bool = false) -> some View {
NavigationLink { HStack{
GroupMemberInfoView(member: member) ProfileImage(imageStr: member.image)
} label: { .frame(width: 38, height: 38)
HStack{ .padding(.trailing, 2)
ProfileImage(imageStr: member.image) // TODO server connection status
.frame(width: 38, height: 38) VStack(alignment: .leading) {
.padding(.trailing, 2) Text(member.chatViewName)
// TODO server connection status .lineLimit(1)
VStack(alignment: .leading) { .foregroundColor(.primary)
Text(member.chatViewName) let s = Text(member.memberStatus.shortText)
.lineLimit(1) (user ? Text ("you: ") + s : s)
Text(member.memberStatus.shortText) .lineLimit(1)
.lineLimit(1) .font(.caption)
.font(.caption) .foregroundColor(.secondary)
.foregroundColor(.secondary) }
} Spacer()
Spacer() let role = member.memberRole
let role = member.memberRole if role == .owner || role == .admin {
if role == .owner || role == .admin { Text(member.memberRole.text)
Text(member.memberRole.text) .foregroundColor(.secondary)
}
} }
} }
} }

View File

@@ -85,7 +85,13 @@ struct AddGroupView: View {
m.chatId = groupInfo.id m.chatId = groupInfo.id
} }
} catch { } catch {
fatalError("Failed to create group: \(responseError(error))") openedSheet = nil
AlertManager.shared.showAlert(
Alert(
title: Text("Failed to create group"),
message: Text(responseError(error))
)
)
} }
} }

View File

@@ -28,7 +28,7 @@ struct NewChatButton: View {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(width: 24, height: 24 ) .frame(width: 24, height: 24)
} }
.confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) { .confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) {
Button("Create link / QR code") { addContactAction() } Button("Create link / QR code") { addContactAction() }

View File

@@ -500,8 +500,8 @@ public struct NetCfg: Codable {
} }
public struct ConnectionStats: Codable { public struct ConnectionStats: Codable {
var rcvServers: [String]? public var rcvServers: [String]?
var sndServers: [String]? public var sndServers: [String]?
} }
public protocol SelectableItem: Hashable, Identifiable { public protocol SelectableItem: Hashable, Identifiable {

View File

@@ -444,6 +444,10 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
return membership.memberRole == .owner || (s == .memRemoved || s == .memLeft || s == .memGroupDeleted || s == .memInvited) 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( public static let sampleData = GroupInfo(
groupId: 1, groupId: 1,
localDisplayName: "team", 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( public static let sampleData = GroupMember(
groupMemberId: 1, groupMemberId: 1,
groupId: 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 member = "member"
case admin = "admin" case admin = "admin"
case owner = "owner" case owner = "owner"
public var id: Self { self }
public var text: LocalizedStringKey { public var text: LocalizedStringKey {
switch self { switch self {
case .member: return "Member" case .member: return "member"
case .admin: return "Admin" case .admin: return "admin"
case .owner: return "Owner" 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 { public enum GroupMemberCategory: String, Decodable {
@@ -576,17 +598,17 @@ public enum GroupMemberStatus: String, Decodable {
public var text: LocalizedStringKey { public var text: LocalizedStringKey {
switch self { switch self {
case .memRemoved: return "Removed" case .memRemoved: return "removed"
case .memLeft: return "Left" case .memLeft: return "left"
case .memGroupDeleted: return "Group deleted" case .memGroupDeleted: return "group deleted"
case .memInvited: return "Invited" case .memInvited: return "invited"
case .memIntroduced: return "Connecting (introduced)" case .memIntroduced: return "connecting (introduced)"
case .memIntroInvited: return "Connecting (introduction invitation)" case .memIntroInvited: return "connecting (introduction invitation)"
case .memAccepted: return "Connecting (accepted)" case .memAccepted: return "connecting (accepted)"
case .memAnnounced: return "Connecting (announced)" case .memAnnounced: return "connecting (announced)"
case .memConnected: return "Connected" case .memConnected: return "connected"
case .memComplete: return "Complete" case .memComplete: return "complete"
case .memCreator: return "Creator" case .memCreator: return "creator"
} }
} }

View File

@@ -745,7 +745,8 @@ processChatCommand = \case
Nothing -> throwChatError CEGroupMemberNotFound Nothing -> throwChatError CEGroupMemberNotFound
Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do
let userRole = memberRole (membership :: GroupMember) let userRole = memberRole (membership :: GroupMember)
when (userRole < GRAdmin || userRole < mRole) $ throwChatError CEGroupUserRole canRemove = userRole >= GRAdmin && userRole >= mRole
unless canRemove $ throwChatError CEGroupUserRole
withChatLock . procCmd $ do withChatLock . procCmd $ do
when (mStatus /= GSMemInvited) $ do when (mStatus /= GSMemInvited) $ do
msg <- sendGroupMessage gInfo members $ XGrpMemDel mId msg <- sendGroupMessage gInfo members $ XGrpMemDel mId

View File

@@ -1102,6 +1102,7 @@ testGroupAsync = withTmpFiles $ do
cath <## "#team: connected to server(s)" cath <## "#team: connected to server(s)"
cath <## "#team: member bob (Bob) is connected" cath <## "#team: member bob (Bob) is connected"
] ]
threadDelay 500000
print (3 :: Integer) print (3 :: Integer)
withTestChat "bob" $ \bob -> do withTestChat "bob" $ \bob -> do
withNewTestChat "dan" danProfile $ \dan -> do withNewTestChat "dan" danProfile $ \dan -> do
@@ -1120,7 +1121,8 @@ testGroupAsync = withTmpFiles $ do
[ bob <## "#team: dan joined the group", [ bob <## "#team: dan joined the group",
dan <## "#team: you joined the group" dan <## "#team: you joined the group"
] ]
threadDelay 500000 threadDelay 1000000
threadDelay 500000
print (4 :: Integer) print (4 :: Integer)
withTestChat "alice" $ \alice -> do withTestChat "alice" $ \alice -> do
withTestChat "cath" $ \cath -> do withTestChat "cath" $ \cath -> do