From a4aaf3677437deef5aa153e06632d55e20da6ebf Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 26 Jul 2022 12:33:10 +0400 Subject: [PATCH] ios: group & group member info views (#841) * ios: group member wip * wip * wip * wip * wip * refactor alerts * .navigationBarHidden(true) * await MainActor.run * refactor * fix * update layout * tex Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/SimpleXAPI.swift | 6 + apps/ios/Shared/Views/Chat/ChatInfoView.swift | 29 ++- apps/ios/Shared/Views/Chat/ChatView.swift | 22 ++- .../Chat/Group/AddGroupMembersView.swift | 8 +- .../Chat/Group/GroupMemberInfoView.swift | 104 ++++++++++ .../Shared/Views/Chat/GroupChatInfoView.swift | 183 ++++++++++++++---- .../Views/ChatList/ChatListNavLink.swift | 25 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/ChatTypes.swift | 71 +++++-- 9 files changed, 364 insertions(+), 88 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index d86a4b279..bdedb5072 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -594,6 +594,12 @@ func apiJoinGroup(_ groupId: Int64) async throws -> GroupInfo { throw r } +func apiRemoveMember(groupId: Int64, memberId: Int64) async throws -> GroupMember { + let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false) + if case let .userDeletedMember(_, member) = r { return member } + throw r +} + func leaveGroup(_ groupId: Int64) async { do { let groupInfo = try await apiLeaveGroup(groupId) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index c9a7b6799..50fc5acab 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -13,10 +13,8 @@ struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var chat: Chat - @Binding var chatViewSheet: ChatViewSheet? + @Binding var showSheet: Bool @State var alert: ChatInfoViewAlert? = nil - @State var deletingContact: Contact? - var contact: Contact enum ChatInfoViewAlert: Identifiable { case deleteContactAlert @@ -26,7 +24,7 @@ struct ChatInfoView: View { } var body: some View { - VStack{ + VStack { ChatInfoImage(chat: chat) .frame(width: 192, height: 192) .padding(.top, 48) @@ -55,7 +53,6 @@ struct ChatInfoView: View { } .tint(Color.orange) Button(role: .destructive) { - deletingContact = contact alert = .deleteContactAlert } label: { Label("Delete contact", systemImage: "trash") @@ -64,7 +61,7 @@ struct ChatInfoView: View { } .alert(item: $alert) { alertItem in switch(alertItem) { - case .deleteContactAlert: return deleteContactAlert(deletingContact!) + case .deleteContactAlert: return deleteContactAlert() case .clearChatAlert: return clearChatAlert() } } @@ -77,20 +74,20 @@ struct ChatInfoView: View { .foregroundColor(status == .connected ? .green : .secondary) } - private func deleteContactAlert(_ contact: Contact) -> Alert { + private func deleteContactAlert() -> Alert { Alert( title: Text("Delete contact?"), message: Text("Contact and all messages will be deleted - this cannot be undone!"), primaryButton: .destructive(Text("Delete")) { Task { do { - try await apiDeleteChat(type: .direct, id: contact.apiId) - DispatchQueue.main.async { - chatModel.removeChat(contact.id) - chatViewSheet = nil + try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) + await MainActor.run { + chatModel.removeChat(chat.chatInfo.id) + showSheet = false } } catch let error { - logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") + logger.error("deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") } } }, @@ -105,8 +102,8 @@ struct ChatInfoView: View { primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) - DispatchQueue.main.async { - chatViewSheet = nil + await MainActor.run { + showSheet = false } } }, @@ -117,7 +114,7 @@ struct ChatInfoView: View { struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { - @State var chatViewSheet = ChatViewSheet.chatInfo - return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), chatViewSheet: Binding($chatViewSheet), contact: Contact.sampleData) + @State var showSheet = true + return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showSheet: $showSheet) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index b8272b0e3..9b45a05a2 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -22,6 +22,7 @@ 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 composeState = ComposeState() @State private var deletingItem: ChatItem? = nil @@ -107,19 +108,21 @@ struct ChatView: View { ToolbarItem(placement: .principal) { Button { chatViewSheet = .chatInfo + showChatViewSheet = true } label: { ChatInfoToolbar(chat: chat) } - .sheet(item: $chatViewSheet) { sheet in - switch sheet { + .sheet(isPresented: $showChatViewSheet) { + switch chatViewSheet { case .chatInfo: - if case let .direct(contact) = chat.chatInfo { - ChatInfoView(chat: chat, chatViewSheet: $chatViewSheet, contact: contact) - } else if case .group = chat.chatInfo { - GroupChatInfoView(chat: chat, chatViewSheet: $chatViewSheet) + 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, chatViewSheet: $chatViewSheet) + AddGroupMembersView(chat: chat, showSheet: $showChatViewSheet) + default: EmptyView() } } } @@ -130,7 +133,7 @@ struct ChatView: View { callButton(contact, .video, imageName: "video") } } else if case .group = chat.chatInfo { - addMemberButton() + addMembersButton() } } } @@ -145,9 +148,10 @@ struct ChatView: View { } } - private func addMemberButton() -> some View { + private func addMembersButton() -> some View { Button { chatViewSheet = .addMember + showChatViewSheet = 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 9be0e5942..55b996c23 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -12,7 +12,7 @@ import SimpleXChat struct AddGroupMembersView: View { @EnvironmentObject var chatModel: ChatModel var chat: Chat - @Binding var chatViewSheet: ChatViewSheet? + @Binding var showSheet: Bool @State private var contactsToAdd: [Contact] = [] @State private var selectedContacts = Set() @@ -35,7 +35,7 @@ struct AddGroupMembersView: View { for contactId in selectedContacts { await addMember(groupId: chat.chatInfo.apiId, contactId: contactId) } - chatViewSheet = nil + showSheet = false } } label: { Label( @@ -103,7 +103,7 @@ struct AddGroupMembersView: View { struct AddGroupMembersView_Previews: PreviewProvider { static var previews: some View { - @State var chatViewSheet = ChatViewSheet.chatInfo - return AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), chatViewSheet: Binding($chatViewSheet)) + @State var showSheet = true + return AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), showSheet: $showSheet) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift new file mode 100644 index 000000000..c9c413151 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -0,0 +1,104 @@ +// +// GroupMemberInfoView.swift +// SimpleX (iOS) +// +// Created by JRoberts on 25.07.2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct GroupMemberInfoView: View { + @EnvironmentObject var chatModel: ChatModel + var member: GroupMember + @State private var alert: GroupMemberInfoViewAlert? = nil + + enum GroupMemberInfoViewAlert: Identifiable { + case removeMemberAlert + + var id: GroupMemberInfoViewAlert { get { self } } + } + + var body: some View { + NavigationView { + List { + groupMemberInfoHeader() + .listRowBackground(Color.clear) + + // TODO server status + + Section(header: Text("Info")) { + Text("Role: ") + Text(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)") + } + } + + Section { + removeMemberButton() + } + } + .navigationBarHidden(true) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .alert(item: $alert) { alertItem in + switch(alertItem) { + case .removeMemberAlert: return removeMemberAlert() + } + } + } + + 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) + .font(.largeTitle) + .lineLimit(1) + .padding(.bottom, 2) + Text(member.fullName) + .font(.title2) + .lineLimit(2) + } + .frame(maxWidth: .infinity, alignment: .center) + } + + func removeMemberButton() -> some View { + Button(role: .destructive) { + alert = .removeMemberAlert + } label: { + Label("Remove member", systemImage: "trash") + .foregroundColor(Color.red) + } + } + + private func removeMemberAlert() -> Alert { + Alert( + title: Text("Remove member?"), + message: Text("Member will be removed from group - this cannot be undone!"), + primaryButton: .destructive(Text("Remove")) { + Task { + do { + _ = try await apiRemoveMember(groupId: member.groupId, memberId: member.groupMemberId) + // TODO navigate back + } catch let error { + logger.error("removeMemberAlert apiRemoveMember error: \(error.localizedDescription)") + } + } + }, + secondaryButton: .cancel() + ) + } +} + +struct GroupMemberInfoView_Previews: PreviewProvider { + static var previews: some View { + GroupMemberInfoView(member: GroupMember.sampleData) + } +} diff --git a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift index d2c353866..a99b18915 100644 --- a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift @@ -13,44 +13,83 @@ struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var chat: Chat - @Binding var chatViewSheet: ChatViewSheet? - @State var alert: ChatInfoViewAlert? = nil - @State var deletingContact: Contact? + var groupInfo: GroupInfo + @Binding var showSheet: Bool + @State private var members: [GroupMember] = [] + @State private var alert: GroupChatInfoViewAlert? = nil + @State private var showAddMembersSheet: Bool = false - enum ChatInfoViewAlert: Identifiable { - case deleteContactAlert + enum GroupChatInfoViewAlert: Identifiable { + case deleteGroupAlert case clearChatAlert + case leaveGroupAlert - var id: ChatInfoViewAlert { get { self } } + var id: GroupChatInfoViewAlert { get { self } } } var body: some View { - VStack{ - ChatInfoImage(chat: chat) - .frame(width: 192, height: 192) - .padding(.top, 48) - .padding() - Text(chat.chatInfo.localDisplayName).font(.largeTitle) - .padding(.bottom, 2) - Text(chat.chatInfo.fullName).font(.title) - .padding(.bottom) + NavigationView { + List { + groupInfoHeader() + .listRowBackground(Color.clear) - Spacer() - Button() { - alert = .clearChatAlert - } label: { - Label("Clear conversation", systemImage: "gobackward") + Section(header: Text("\(members.count) Members")) { + addMembersButton() + ForEach(members) { member in + memberView(member) + } + } + + Section { + clearChatButton() + if groupInfo.canDelete { + deleteGroupButton() + } + leaveGroupButton() + } } - .tint(Color.orange) - .padding() + .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 .deleteContactAlert: return deleteContactAlert(deletingContact!) + case .deleteGroupAlert: return deleteGroupAlert() case .clearChatAlert: return clearChatAlert() + case .leaveGroupAlert: return leaveGroupAlert() } } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .task { + members = await apiListMembers(chat.chatInfo.apiId) + .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } // TODO owner first + } + } + + func groupInfoHeader() -> some View { + VStack { + ChatInfoImage(chat: chat, color: Color(uiColor: .tertiarySystemFill)) + .frame(width: 192, height: 192) + .padding(.top, 12) + .padding() + Text(chat.chatInfo.localDisplayName) + .font(.largeTitle) + .lineLimit(1) + .padding(.bottom, 2) + Text(chat.chatInfo.fullName) + .font(.title2) + .lineLimit(2) + } + .frame(maxWidth: .infinity, alignment: .center) + } + + private func addMembersButton() -> some View { + Button { + showAddMembersSheet = true + } label: { + Label("Invite members", systemImage: "plus") + } } func serverImage() -> some View { @@ -59,20 +98,75 @@ struct GroupChatInfoView: View { .foregroundColor(status == .connected ? .green : .secondary) } - private func deleteContactAlert(_ contact: Contact) -> Alert { + 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 deleteGroupButton() -> some View { + Button(role: .destructive) { + alert = .deleteGroupAlert + } label: { + Label("Delete group", systemImage: "trash") + .foregroundColor(Color.red) + } + } + + func clearChatButton() -> some View { + Button() { + alert = .clearChatAlert + } label: { + Label("Clear conversation", systemImage: "gobackward") + .foregroundColor(Color.orange) + } + .tint(Color.orange) + } + + func leaveGroupButton() -> some View { + Button(role: .destructive) { + alert = .leaveGroupAlert + } label: { + Label("Leave group", systemImage: "rectangle.portrait.and.arrow.right") + .foregroundColor(Color.red) + } + } + + // TODO reuse this and clearChatAlert with ChatInfoView + private func deleteGroupAlert() -> Alert { Alert( - title: Text("Delete contact?"), - message: Text("Contact and all messages will be deleted - this cannot be undone!"), + title: Text("Delete group?"), + message: Text("Group will be deleted for all members - this cannot be undone!"), primaryButton: .destructive(Text("Delete")) { Task { do { - try await apiDeleteChat(type: .direct, id: contact.apiId) - DispatchQueue.main.async { - chatModel.removeChat(contact.id) - chatViewSheet = nil + try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) + await MainActor.run { + chatModel.removeChat(chat.chatInfo.id) + showSheet = false } } catch let error { - logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") + logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)") } } }, @@ -80,7 +174,6 @@ struct GroupChatInfoView: View { ) } - // TODO reuse between this and ChatInfoView private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -88,8 +181,24 @@ struct GroupChatInfoView: View { primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) - DispatchQueue.main.async { - chatViewSheet = nil + await MainActor.run { + showSheet = false + } + } + }, + secondaryButton: .cancel() + ) + } + + private func leaveGroupAlert() -> Alert { + Alert( + title: Text("Leave group?"), + message: Text("You will stop receiving messages from this group. Chat history will be preserved."), + primaryButton: .destructive(Text("Leave")) { + Task { + await leaveGroup(chat.chatInfo.apiId) + await MainActor.run { + showSheet = false } } }, @@ -100,7 +209,7 @@ struct GroupChatInfoView: View { struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { - @State var chatViewSheet = ChatViewSheet.chatInfo - return GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), chatViewSheet: Binding($chatViewSheet)) + @State var showSheet = true + return GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData, showSheet: $showSheet) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 0f594235f..08f9c7b98 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -53,7 +53,7 @@ struct ChatListNavLink: View { Button(role: .destructive) { AlertManager.shared.showAlert( contact.ready - ? deleteChatAlert(chat.chatInfo) + ? deleteContactAlert(chat.chatInfo) : deletePendingContactAlert(chat, contact) ) } label: { @@ -80,7 +80,7 @@ struct ChatListNavLink: View { joinGroupButton() } .swipeActions(edge: .trailing) { - if groupInfo.canDelete() { + if groupInfo.canDelete { deleteGroupChatButton(groupInfo) } } @@ -123,7 +123,7 @@ struct ChatListNavLink: View { } } .swipeActions(edge: .trailing) { - if groupInfo.canDelete() { + if groupInfo.canDelete { deleteGroupChatButton(groupInfo) } } @@ -159,7 +159,7 @@ struct ChatListNavLink: View { @ViewBuilder private func deleteGroupChatButton(_ groupInfo: GroupInfo) -> some View { Button(role: .destructive) { - AlertManager.shared.showAlert(deleteChatAlert(.group(groupInfo: groupInfo))) + AlertManager.shared.showAlert(deleteGroupAlert(.group(groupInfo: groupInfo))) } label: { Label("Delete", systemImage: "trash") } @@ -210,10 +210,21 @@ struct ChatListNavLink: View { } } - private func deleteChatAlert(_ chatInfo: ChatInfo) -> Alert { + private func deleteContactAlert(_ chatInfo: ChatInfo) -> Alert { Alert( - title: Text("Delete chat?"), - message: Text("Chat and all messages will be deleted - this cannot be undone!"), + title: Text("Delete contact?"), + message: Text("Contact and all messages will be deleted - this cannot be undone!"), + primaryButton: .destructive(Text("Delete")) { + Task { await deleteChat(chat) } + }, + secondaryButton: .cancel() + ) + } + + private func deleteGroupAlert(_ chatInfo: ChatInfo) -> Alert { + Alert( + title: Text("Delete group?"), + message: Text("Group will be deleted for all members - this cannot be undone!"), primaryButton: .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 01c6cca0b..d56732414 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; }; 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; }; + 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; @@ -295,6 +296,7 @@ 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; }; 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = ""; }; + 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberInfoView.swift; sourceTree = ""; }; 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; @@ -629,6 +631,7 @@ isa = PBXGroup; children = ( 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */, + 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */, ); path = Group; sourceTree = ""; @@ -869,6 +872,7 @@ 5CB0BA962827143500B3292C /* MakeConnection.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, + 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 780d2a25a..ee8778d2f 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -284,7 +284,7 @@ public struct ContactSubStatus: Decodable { public struct Connection: Decodable { var connId: Int64 var connStatus: ConnStatus - var connLevel: Int + public var connLevel: Int public var id: ChatId { get { ":\(connId)" } } @@ -439,12 +439,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } - public func canDelete() -> Bool { + public var canDelete: Bool { let s = membership.memberStatus return membership.memberRole == .owner || (s == .memRemoved || s == .memLeft || s == .memGroupDeleted || s == .memInvited) } - static let sampleData = GroupInfo( + public static let sampleData = GroupInfo( groupId: 1, localDisplayName: "team", groupProfile: GroupProfile.sampleData, @@ -471,18 +471,23 @@ public struct GroupProfile: Codable, NamedChat { ) } -public struct GroupMember: Decodable { +public struct GroupMember: Identifiable, Decodable { public var groupMemberId: Int64 - var groupId: Int64 - var memberId: String - var memberRole: GroupMemberRole - var memberCategory: GroupMemberCategory + public var groupId: Int64 + public var memberId: String + public var memberRole: GroupMemberRole + public var memberCategory: GroupMemberCategory public var memberStatus: GroupMemberStatus - var invitedBy: InvitedBy - var localDisplayName: ContactName + public var invitedBy: InvitedBy + public var localDisplayName: ContactName public var memberProfile: Profile public var memberContactId: Int64? - var activeConn: Connection? + public var activeConn: Connection? + + public var id: String { "#\(groupId) @\(groupMemberId)" } + public var displayName: String { get { memberProfile.displayName } } + public var fullName: String { get { memberProfile.fullName } } + public var image: String? { get { memberProfile.image } } var directChatId: ChatId? { get { @@ -494,10 +499,6 @@ public struct GroupMember: Decodable { } } - public var id: String { - "#\(groupId) @\(groupMemberId)" - } - public var chatViewName: String { get { let p = memberProfile @@ -542,6 +543,14 @@ public enum GroupMemberRole: String, Decodable { case member = "member" case admin = "admin" case owner = "owner" + + public var text: LocalizedStringKey { + switch self { + case .member: return "Member" + case .admin: return "Admin" + case .owner: return "Owner" + } + } } public enum GroupMemberCategory: String, Decodable { @@ -564,6 +573,38 @@ public enum GroupMemberStatus: String, Decodable { case memConnected = "connected" case memComplete = "complete" case memCreator = "creator" + + 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" + } + } + + public var shortText: LocalizedStringKey { + switch self { + case .memRemoved: return "removed" + case .memLeft: return "left" + case .memGroupDeleted: return "group deleted" + case .memInvited: return "invited" + case .memIntroduced: return "connecting" + case .memIntroInvited: return "connecting" + case .memAccepted: return "connecting" + case .memAnnounced: return "connecting" + case .memConnected: return "connected" + case .memComplete: return "complete" + case .memCreator: return "creator" + } + } } public enum InvitedBy: Decodable {