From 608030dcaf09639ae7cf4a2a1716a16e879eb590 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 26 Jul 2022 10:55:58 +0400 Subject: [PATCH] ios: add member ui wip (#834) * ios: add member ui wip * AddGroupMembersView * clean up * cleanup * change new chat button * update adding members * add group name and image to adding members view * adjust layout * layout Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/ContentView.swift | 13 +-- apps/ios/Shared/Model/ChatModel.swift | 1 - apps/ios/Shared/Model/SimpleXAPI.swift | 38 +++++- .../Shared/Views/Chat/ChatInfoToolbar.swift | 3 +- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 10 +- .../Chat/ChatItem/CIGroupInvitationView.swift | 2 +- apps/ios/Shared/Views/Chat/ChatView.swift | 38 ++++-- .../Chat/Group/AddGroupMembersView.swift | 109 ++++++++++++++++++ .../Shared/Views/Chat/GroupChatInfoView.swift | 10 +- .../Views/ChatList/ChatListNavLink.swift | 16 ++- .../Shared/Views/ChatList/ChatListView.swift | 8 +- .../Shared/Views/NewChat/AddGroupView.swift | 2 + .../Shared/Views/NewChat/NewChatButton.swift | 7 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 12 ++ apps/ios/SimpleXChat/APITypes.swift | 6 + apps/ios/SimpleXChat/ChatTypes.swift | 15 ++- 16 files changed, 237 insertions(+), 53 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index f1511a66e6..0d562ba79c 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -13,7 +13,6 @@ struct ContentView: View { @ObservedObject var callController = CallController.shared @Binding var doAuthenticate: Bool @Binding var userAuthorized: Bool? - @State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication @AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -42,7 +41,7 @@ struct ContentView: View { private func mainView() -> some View { ZStack(alignment: .top) { - ChatListView(showChatInfo: $showChatInfo) + ChatListView() .onAppear { NtfManager.shared.requestAuthorization(onDeny: { alertManager.showAlert(notificationAlert()) @@ -64,11 +63,11 @@ struct ContentView: View { private func runAuthenticate() { if !prefPerformLA { userAuthorized = true - } else if showChatInfo { - showChatInfo = false - DispatchQueue.main.async { - justAuthenticate() - } +// } else if showChatInfo { +// showChatInfo = false +// DispatchQueue.main.async { +// justAuthenticate() +// } } else { justAuthenticate() } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 775d58d81f..61acc502db 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -24,7 +24,6 @@ final class ChatModel: ObservableObject { @Published var chatId: String? @Published var chatItems: [ChatItem] = [] @Published var chatToTop: String? -// @Published var groups: Dictionary = [:] // items in the terminal view @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: String? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index af6785b66a..d86a4b2799 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -402,6 +402,12 @@ func clearChat(_ chat: Chat) async { } } +func apiListContacts() throws -> [Contact] { + let r = chatSendCmdSync(.listContacts) + if case let .contactsList(contacts) = r { return contacts } + throw r +} + func apiUpdateProfile(profile: Profile) async throws -> Profile? { let r = await chatSendCmd(.apiUpdateProfile(profile: profile)) switch r { @@ -559,36 +565,56 @@ func apiNewGroup(_ gp: GroupProfile) throws -> GroupInfo { throw r } -func joinGroup(groupId: Int64) async { +func addMember(groupId: Int64, contactId: Int64) async { do { - let groupInfo = try await apiJoinGroup(groupId: groupId) + try await apiAddMember(groupId: groupId, contactId: contactId, memberRole: .admin) + } catch let error { + logger.error("addMember error: \(responseError(error))") + } +} + +func apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) async throws { + let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole)) + if case .sentGroupInvitation = r { return } + throw r +} + +func joinGroup(_ groupId: Int64) async { + do { + let groupInfo = try await apiJoinGroup(groupId) DispatchQueue.main.async { ChatModel.shared.updateGroup(groupInfo) } } catch let error { logger.error("joinGroup error: \(responseError(error))") } } -func apiJoinGroup(groupId: Int64) async throws -> GroupInfo { +func apiJoinGroup(_ groupId: Int64) async throws -> GroupInfo { let r = await chatSendCmd(.apiJoinGroup(groupId: groupId)) if case let .userAcceptedGroupSent(groupInfo) = r { return groupInfo } throw r } -func leaveGroup(groupId: Int64) async { +func leaveGroup(_ groupId: Int64) async { do { - let groupInfo = try await apiLeaveGroup(groupId: groupId) + let groupInfo = try await apiLeaveGroup(groupId) DispatchQueue.main.async { ChatModel.shared.updateGroup(groupInfo) } } catch let error { logger.error("leaveGroup error: \(responseError(error))") } } -func apiLeaveGroup(groupId: Int64) async throws -> GroupInfo { +func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo { let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false) if case let .leftMemberUser(groupInfo) = r { return groupInfo } throw r } +func apiListMembers(_ groupId: Int64) async -> [GroupMember] { + let r = await chatSendCmd(.apiListMembers(groupId: groupId)) + if case let .groupMembers(group) = r { return group.members } + return [] +} + func initializeChat(start: Bool) throws { logger.debug("initializeChat") do { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 44be65ec95..a5fe3b61d7 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -14,6 +14,7 @@ let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2 struct ChatInfoToolbar: View { @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat + var imageSize: CGFloat = 32 var body: some View { let cInfo = chat.chatInfo @@ -24,7 +25,7 @@ struct ChatInfoToolbar: View { ? chatImageColorDark : chatImageColorLight ) - .frame(width: 32, height: 32) + .frame(width: imageSize, height: imageSize) .padding(.trailing, 4) VStack { Text(cInfo.displayName).font(.headline) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 3e96348f02..c9a7b67996 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -13,7 +13,7 @@ struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var chat: Chat - @Binding var showChatInfo: Bool + @Binding var chatViewSheet: ChatViewSheet? @State var alert: ChatInfoViewAlert? = nil @State var deletingContact: Contact? var contact: Contact @@ -87,7 +87,7 @@ struct ChatInfoView: View { try await apiDeleteChat(type: .direct, id: contact.apiId) DispatchQueue.main.async { chatModel.removeChat(contact.id) - showChatInfo = false + chatViewSheet = nil } } catch let error { logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") @@ -106,7 +106,7 @@ struct ChatInfoView: View { Task { await clearChat(chat) DispatchQueue.main.async { - showChatInfo = false + chatViewSheet = nil } } }, @@ -117,7 +117,7 @@ struct ChatInfoView: View { struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { - @State var showChatInfo = true - return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo, contact: Contact.sampleData) + @State var chatViewSheet = ChatViewSheet.chatInfo + return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), chatViewSheet: Binding($chatViewSheet), contact: Contact.sampleData) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 803ff19939..4419a884d4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -98,7 +98,7 @@ struct CIGroupInvitationView: View { private func acceptInvitation() { Task { logger.debug("acceptInvitation") - await joinGroup(groupId: groupInvitation.groupId) + await joinGroup(groupInvitation.groupId) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 6b3e827d74..b8272b0e37 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -11,11 +11,18 @@ 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 - @Binding var showChatInfo: Bool + @State private var chatViewSheet: ChatViewSheet? @State private var composeState = ComposeState() @State private var deletingItem: ChatItem? = nil @FocusState private var keyboardVisible: Bool @@ -99,15 +106,20 @@ struct ChatView: View { } ToolbarItem(placement: .principal) { Button { - showChatInfo = true + chatViewSheet = .chatInfo } label: { ChatInfoToolbar(chat: chat) } - .sheet(isPresented: $showChatInfo) { - if case let .direct(contact) = chat.chatInfo { - ChatInfoView(chat: chat, showChatInfo: $showChatInfo, contact: contact) - } else if case .group = chat.chatInfo { - GroupChatInfoView(chat: chat, showChatInfo: $showChatInfo) + .sheet(item: $chatViewSheet) { sheet in + switch sheet { + 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) + } + case .addMember: + AddGroupMembersView(chat: chat, chatViewSheet: $chatViewSheet) } } } @@ -117,6 +129,8 @@ struct ChatView: View { callButton(contact, .audio, imageName: "phone") callButton(contact, .video, imageName: "video") } + } else if case .group = chat.chatInfo { + addMemberButton() } } } @@ -131,6 +145,14 @@ struct ChatView: View { } } + private func addMemberButton() -> some View { + Button { + chatViewSheet = .addMember + } label: { + Image(systemName: "person.crop.circle.badge.plus") + } + } + private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading return ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth) @@ -276,7 +298,7 @@ struct ChatView_Previews: PreviewProvider { ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") ] @State var showChatInfo = false - return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo) + return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift new file mode 100644 index 0000000000..9be0e5942a --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -0,0 +1,109 @@ +// +// AddGroupMembersView.swift +// SimpleX (iOS) +// +// Created by JRoberts on 22.07.2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct AddGroupMembersView: View { + @EnvironmentObject var chatModel: ChatModel + var chat: Chat + @Binding var chatViewSheet: ChatViewSheet? + @State private var contactsToAdd: [Contact] = [] + @State private var selectedContacts = Set() + + 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) + } + chatViewSheet = nil + } + } 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) + } + .listStyle(.plain) + } + } + .frame(maxHeight: .infinity, alignment: .top) + .task { + contactsToAdd = await getContactsToAdd() + } + } + + func getContactsToAdd() async -> [Contact] { + let memberContactIds = await apiListMembers(chat.chatInfo.apiId) + .compactMap{ $0.memberContactId } + return chatModel.chats + .compactMap{ $0.chatInfo.contact } + .filter{ !memberContactIds.contains($0.apiId) } + .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } + } + + func contactCheckView(_ contact: Contact) -> some View { + let checked = selectedContacts.contains(contact.apiId) + return Button { + if checked { + selectedContacts.remove(contact.apiId) + } else { + selectedContacts.insert(contact.apiId) + } + } label: { + HStack{ + ProfileImage(imageStr: contact.image) + .frame(width: 30, height: 30) + .padding(.trailing, 2) + Text(ChatInfo.direct(contact: contact).chatViewName) + .lineLimit(1) + Spacer() + Image(systemName: checked ? "checkmark.circle.fill": "circle") + .foregroundColor(checked ? .accentColor : .secondary) + } + } + } +} + +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)) + } +} diff --git a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift index 58795fb2a2..d2c353866c 100644 --- a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift @@ -13,7 +13,7 @@ struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var alertManager = AlertManager.shared @ObservedObject var chat: Chat - @Binding var showChatInfo: Bool + @Binding var chatViewSheet: ChatViewSheet? @State var alert: ChatInfoViewAlert? = nil @State var deletingContact: Contact? @@ -69,7 +69,7 @@ struct GroupChatInfoView: View { try await apiDeleteChat(type: .direct, id: contact.apiId) DispatchQueue.main.async { chatModel.removeChat(contact.id) - showChatInfo = false + chatViewSheet = nil } } catch let error { logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") @@ -89,7 +89,7 @@ struct GroupChatInfoView: View { Task { await clearChat(chat) DispatchQueue.main.async { - showChatInfo = false + chatViewSheet = nil } } }, @@ -100,7 +100,7 @@ struct GroupChatInfoView: View { struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { - @State var showChatInfo = true - return GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo) + @State var chatViewSheet = ChatViewSheet.chatInfo + return GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), chatViewSheet: Binding($chatViewSheet)) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index e12de4a8d0..0f594235f7 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -12,7 +12,6 @@ import SimpleXChat struct ChatListNavLink: View { @EnvironmentObject var chatModel: ChatModel @State var chat: Chat - @Binding var showChatInfo: Bool @State private var showContactRequestDialog = false @State private var showJoinGroupDialog = false @@ -30,7 +29,7 @@ struct ChatListNavLink: View { } private func chatView() -> some View { - ChatView(chat: chat, showChatInfo: $showChatInfo) + ChatView(chat: chat) .onAppear { loadChat(chat: chat) } } @@ -87,7 +86,7 @@ struct ChatListNavLink: View { } .onTapGesture { showJoinGroupDialog = true } .confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) { - Button("Join group") { Task { await joinGroup(groupId: groupInfo.groupId) } } + Button("Join group") { Task { await joinGroup(groupInfo.groupId) } } Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } } } case .memAccepted: @@ -133,7 +132,7 @@ struct ChatListNavLink: View { private func joinGroupButton() -> some View { Button { - Task { await joinGroup(groupId: chat.chatInfo.apiId) } + Task { await joinGroup(chat.chatInfo.apiId) } } label: { Label("Join", systemImage: "ipad.and.arrow.forward") } @@ -238,7 +237,7 @@ struct ChatListNavLink: View { 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(groupId: groupInfo.groupId) } + Task { await leaveGroup(groupInfo.groupId) } }, secondaryButton: .cancel() ) @@ -324,20 +323,19 @@ struct ChatListNavLink: View { struct ChatListNavLink_Previews: PreviewProvider { static var previews: some View { @State var chatId: String? = "@1" - @State var showChatInfo = false return Group { ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] - ), showChatInfo: $showChatInfo) + )) ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] - ), showChatInfo: $showChatInfo) + )) ChatListNavLink(chat: Chat( chatInfo: ChatInfo.sampleData.contactRequest, chatItems: [] - ), showChatInfo: $showChatInfo) + )) } .previewLayout(.fixed(width: 360, height: 80)) } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 29850a6e89..2aabdc0fb1 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -11,7 +11,6 @@ import SimpleXChat struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel - @Binding var showChatInfo: Bool // not really used in this view @State private var showSettings = false @State private var searchText = "" @@ -21,7 +20,7 @@ struct ChatListView: View { let v = NavigationView { List { ForEach(filteredChats(), id: \.viewId) { chat in - ChatListNavLink(chat: chat, showChatInfo: $showChatInfo) + ChatListNavLink(chat: chat) .padding(.trailing, -16) .disabled(chatModel.chatRunning != true) } @@ -108,11 +107,10 @@ struct ChatListView_Previews: PreviewProvider { ) ] - @State var showChatInfo = false return Group { - ChatListView(showChatInfo: $showChatInfo) + ChatListView() .environmentObject(chatModel) - ChatListView(showChatInfo: $showChatInfo) + ChatListView() .environmentObject(ChatModel()) } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index e571367dc8..5117cfc3ab 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -22,6 +22,8 @@ struct AddGroupView: View { Text("Create group") .font(.largeTitle) .padding(.bottom, 4) + Text("Enter group details") + .padding(.bottom) ZStack(alignment: .topLeading) { if !validDisplayName(displayName) { Image(systemName: "exclamationmark.circle") diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index e99ebc391d..c2e6624092 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -25,13 +25,16 @@ struct NewChatButton: View { var body: some View { Button { showAddChat = true } label: { - Image(systemName: "person.crop.circle.badge.plus") + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24 ) } .confirmationDialog("Add contact to start a new chat", isPresented: $showAddChat, titleVisibility: .visible) { Button("Create link / QR code") { addContactAction() } Button("Paste received link") { actionSheet = .pasteLink } Button("Scan QR code") { actionSheet = .scanQRCode } - // Button("Create group") { actionSheet = .createGroup } + Button("Create group") { actionSheet = .createGroup } } .sheet(item: $actionSheet) { sheet in switch sheet { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index ab2144ee2f..01c6cca0b0 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -112,6 +112,7 @@ 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; 6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIGroupEventView.swift */; }; + 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; @@ -288,6 +289,7 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIGroupEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupEventView.swift; sourceTree = ""; }; + 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; @@ -379,6 +381,7 @@ 5C5F4AC227A5E9AF00B51EF1 /* Chat */ = { isa = PBXGroup; children = ( + 6440CA01288AEC770062C672 /* Group */, 5CE4407427ADB657007B033A /* ChatItem */, 5CEACCE527DE977C000BD591 /* ComposeMessage */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */, @@ -622,6 +625,14 @@ path = Database; sourceTree = ""; }; + 6440CA01288AEC770062C672 /* Group */ = { + isa = PBXGroup; + children = ( + 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */, + ); + path = Group; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -812,6 +823,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 40cb1246f4..4f46438daa 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -50,6 +50,7 @@ public enum ChatCommand { case connect(connReq: String) case apiDeleteChat(type: ChatType, id: Int64) case apiClearChat(type: ChatType, id: Int64) + case listContacts case apiUpdateProfile(profile: Profile) case createMyAddress case deleteMyAddress @@ -110,6 +111,7 @@ public enum ChatCommand { case let .connect(connReq): return "/connect \(connReq)" case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" + case .listContacts: return "/contacts" case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))" case .createMyAddress: return "/address" case .deleteMyAddress: return "/delete_address" @@ -170,6 +172,7 @@ public enum ChatCommand { case .connect: return "connect" case .apiDeleteChat: return "apiDeleteChat" case .apiClearChat: return "apiClearChat" + case .listContacts: return "listContacts" case .apiUpdateProfile: return "apiUpdateProfile" case .createMyAddress: return "createMyAddress" case .deleteMyAddress: return "deleteMyAddress" @@ -246,6 +249,7 @@ public enum ChatResponse: Decodable, Error { case chatItemStatusUpdated(chatItem: AChatItem) case chatItemUpdated(chatItem: AChatItem) case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem) + case contactsList(contacts: [Contact]) // group events case groupCreated(groupInfo: GroupInfo) case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact) @@ -335,6 +339,7 @@ public enum ChatResponse: Decodable, Error { case .chatItemStatusUpdated: return "chatItemStatusUpdated" case .chatItemUpdated: return "chatItemUpdated" case .chatItemDeleted: return "chatItemDeleted" + case .contactsList: return "contactsList" case .groupCreated: return "groupCreated" case .sentGroupInvitation: return "sentGroupInvitation" case .userAcceptedGroupSent: return "userAcceptedGroupSent" @@ -424,6 +429,7 @@ public enum ChatResponse: Decodable, Error { case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem) case let .chatItemUpdated(chatItem): return String(describing: chatItem) case let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))" + case let .contactsList(contacts): return String(describing: contacts) case let .groupCreated(groupInfo): return String(describing: groupInfo) case let .sentGroupInvitation(groupInfo, contact): return "groupInfo: \(groupInfo)\ncontact: \(contact)" case let .userAcceptedGroupSent(groupInfo): return String(describing: groupInfo) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1fe55e69c8..780d2a25ad 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -180,6 +180,15 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var contact: Contact? { + get { + switch self { + case let .direct(contact): return contact + default: return nil + } + } + } + var createdAt: Date { switch self { case let .direct(contact): return contact.createdAt @@ -410,8 +419,8 @@ public enum ConnStatus: String, Decodable { } public struct Group: Decodable { - var groupInfo: GroupInfo - var members: [GroupMember] + public var groupInfo: GroupInfo + public var members: [GroupMember] } public struct GroupInfo: Identifiable, Decodable, NamedChat { @@ -472,7 +481,7 @@ public struct GroupMember: Decodable { var invitedBy: InvitedBy var localDisplayName: ContactName public var memberProfile: Profile - var memberContactId: Int64? + public var memberContactId: Int64? var activeConn: Connection? var directChatId: ChatId? {