From de0f231c603625a4fe87a11daee7bd400e4c5907 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 30 Jul 2022 13:03:44 +0100 Subject: [PATCH] ios: edit group profile (#853) --- apps/ios/Shared/Model/SimpleXAPI.swift | 14 ++ apps/ios/Shared/Views/Chat/ChatInfoView.swift | 11 +- apps/ios/Shared/Views/Chat/ChatView.swift | 40 +++-- .../Chat/Group/AddGroupMembersView.swift | 25 +--- .../Chat/Group/GroupMemberInfoView.swift | 2 +- .../Views/Chat/Group/GroupProfileView.swift | 140 ++++++++++++++++++ .../Shared/Views/Chat/GroupChatInfoView.swift | 39 +++-- .../Views/UserSettings/UserProfile.swift | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + .../xcschemes/SimpleX (iOS).xcscheme | 1 + apps/ios/SimpleXChat/APITypes.swift | 6 + apps/ios/SimpleXChat/ChatTypes.swift | 11 +- 12 files changed, 239 insertions(+), 56 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a1dd0eeb9..255f37b5c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -621,6 +621,20 @@ func apiListMembers(_ groupId: Int64) async -> [GroupMember] { return [] } +func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] { + let memberContactIds = ms.compactMap{ m in m.memberCurrent ? m.memberContactId : nil } + return ChatModel.shared.chats + .compactMap{ $0.chatInfo.contact } + .filter{ !memberContactIds.contains($0.apiId) } + .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } +} + +func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo { + let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile)) + if case let .groupUpdated(toGroup) = r { return toGroup } + throw r +} + func initializeChat(start: Bool) throws { logger.debug("initializeChat") do { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 49050b1d7..a06b4a165 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -44,8 +44,8 @@ private func serverHost(_ s: String) -> String { struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat - @Binding var showSheet: Bool @State private var alert: ChatInfoViewAlert? = nil @State private var connectionStats: ConnectionStats? @@ -172,7 +172,7 @@ struct ChatInfoView: View { try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) await MainActor.run { chatModel.removeChat(chat.chatInfo.id) - showSheet = false + dismiss() } } catch let error { logger.error("deleteContactAlert apiDeleteChat error: \(error.localizedDescription)") @@ -190,9 +190,7 @@ struct ChatInfoView: View { primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) - await MainActor.run { - showSheet = false - } + await MainActor.run { dismiss() } } }, secondaryButton: .cancel() @@ -209,7 +207,6 @@ struct ChatInfoView: View { struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { - @State var showSheet = true - return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showSheet: $showSheet) + ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 19849a4a9..786889c71 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -17,6 +17,7 @@ struct ChatView: View { @ObservedObject var chat: Chat @State private var showChatInfoSheet: Bool = false @State private var showAddMembersSheet: Bool = false + @State private var membersToAdd: [Contact] = [] @State private var composeState = ComposeState() @State private var deletingItem: ChatItem? = nil @FocusState private var keyboardVisible: Bool @@ -105,25 +106,32 @@ struct ChatView: View { ChatInfoToolbar(chat: chat) } .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) + switch cInfo { + case .direct: + ChatInfoView(chat: chat) + case let .group(groupInfo): + GroupChatInfoView(chat: chat, groupInfo: groupInfo) + default: + EmptyView() } } } ToolbarItem(placement: .navigationBarTrailing) { - if case let .direct(contact) = cInfo { + switch cInfo { + case let .direct(contact): HStack { callButton(contact, .audio, imageName: "phone") callButton(contact, .video, imageName: "video") } - } else if case let .group(groupInfo) = chat.chatInfo, - groupInfo.canAddMembers { - addMembersButton() - .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet) - } + case let .group(groupInfo): + if groupInfo.canAddMembers { + addMembersButton() + .sheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo, membersToAdd: membersToAdd) + } + } + default: + EmptyView() } } } @@ -140,7 +148,15 @@ struct ChatView: View { private func addMembersButton() -> some View { Button { - showAddMembersSheet = true + if case let .group(gInfo) = chat.chatInfo { + Task { + let ms = await apiListMembers(gInfo.apiId) + await MainActor.run { + membersToAdd = filterMembersToAdd(ms) + 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 69f436e1e..6d4e4653a 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -11,10 +11,10 @@ import SimpleXChat struct AddGroupMembersView: View { @EnvironmentObject var chatModel: ChatModel + @Environment(\.dismiss) var dismiss: DismissAction var chat: Chat var groupInfo: GroupInfo - @Binding var showSheet: Bool - @State private var contactsToAdd: [Contact] = [] + @State var membersToAdd: [Contact] @State private var selectedContacts = Set() @State private var selectedRole: GroupMemberRole = .admin @@ -26,7 +26,7 @@ struct AddGroupMembersView: View { .listRowBackground(Color.clear) .listRowSeparator(.hidden) - if (contactsToAdd.isEmpty) { + if (membersToAdd.isEmpty) { Text("No contacts to add") .foregroundColor(.secondary) .padding() @@ -52,7 +52,7 @@ struct AddGroupMembersView: View { } Section { - ForEach(contactsToAdd) { contact in + ForEach(membersToAdd) { contact in contactCheckView(contact) } } @@ -61,18 +61,6 @@ struct AddGroupMembersView: View { .navigationBarHidden(true) } .frame(maxHeight: .infinity, alignment: .top) - .task { - contactsToAdd = await getContactsToAdd() - } - } - - func getContactsToAdd() async -> [Contact] { - let ms = await apiListMembers(chat.chatInfo.apiId) - let memberContactIds = ms.compactMap{ m in m.memberCurrent ? m.memberContactId : nil } - return chatModel.chats - .compactMap{ $0.chatInfo.contact } - .filter{ !memberContactIds.contains($0.apiId) } - .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } func inviteMembersButton() -> some View { @@ -81,7 +69,7 @@ struct AddGroupMembersView: View { for contactId in selectedContacts { await addMember(groupId: chat.chatInfo.apiId, contactId: contactId, memberRole: selectedRole) } - showSheet = false + await MainActor.run { dismiss() } } } label: { HStack { @@ -128,7 +116,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), groupInfo: GroupInfo.sampleData, showSheet: $showSheet) + AddGroupMembersView(chat: Chat(chatInfo: ChatInfo.sampleData.group), groupInfo: GroupInfo.sampleData, membersToAdd: []) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 62c9bd67f..40b283881 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -126,6 +126,6 @@ struct GroupMemberInfoView: View { struct GroupMemberInfoView_Previews: PreviewProvider { static var previews: some View { - return GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData) + GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift new file mode 100644 index 000000000..1548fbe78 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -0,0 +1,140 @@ +// +// GroupProfileView.swift +// SimpleX (iOS) +// +// Created by Evgeny on 29/07/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct GroupProfileView: View { + @EnvironmentObject var chatModel: ChatModel + @Environment(\.dismiss) var dismiss: DismissAction + var groupId: Int64 + @State var groupProfile: GroupProfile + @State private var showChooseSource = false + @State private var showImagePicker = false + @State private var showTakePhoto = false + @State private var chosenImage: UIImage? = nil + @State private var showSaveErrorAlert = false + @State private var saveGroupError: String? = nil + @FocusState private var focusDisplayName + + var body: some View { + return VStack(alignment: .leading) { + Text("Group profile is stored on members devices.\nSimpleX servers cannot see group profile.") + .padding(.bottom) + + ZStack(alignment: .center) { + ZStack(alignment: .topTrailing) { + profileImageView(groupProfile.image) + if groupProfile.image != nil { + Button { + groupProfile.image = nil + } label: { + Image(systemName: "multiply") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12) + } + } + } + + editImageButton { showChooseSource = true } + } + .frame(maxWidth: .infinity, alignment: .center) + + VStack(alignment: .leading) { + ZStack(alignment: .leading) { + if !validDisplayName(groupProfile.displayName) { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + .padding(.bottom, 10) + } + profileNameTextEdit("Group display name", $groupProfile.displayName) + .focused($focusDisplayName) + } + profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) + HStack(spacing: 20) { + Button("Cancel") { dismiss() } + Button("Save") { saveProfile() } + .disabled(groupProfile.displayName == "" || !validDisplayName(groupProfile.displayName)) + } + } + .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) + + } + .padding() + .frame(maxHeight: .infinity, alignment: .top) + .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { + Button("Take picture") { + showTakePhoto = true + } + Button("Choose from library") { + showImagePicker = true + } + } + .fullScreenCover(isPresented: $showTakePhoto) { + ZStack { + Color.black.edgesIgnoringSafeArea(.all) + CameraImagePicker(image: $chosenImage) + } + } + .sheet(isPresented: $showImagePicker) { + LibraryImagePicker(image: $chosenImage) { + didSelectItem in showImagePicker = false + } + } + .onChange(of: chosenImage) { image in + if let image = image { + groupProfile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + groupProfile.image = nil + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + focusDisplayName = true + } + } + .alert(isPresented: $showSaveErrorAlert) { + Alert( + title: Text("Error saving group profile"), + message: Text("\(saveGroupError ?? "Unexpected error")") + ) + } + } + + func profileNameTextEdit(_ label: String, _ name: Binding) -> some View { + TextField(label, text: name) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .padding(.bottom) + .padding(.leading, 28) + } + + func saveProfile() { + Task { + do { + let gInfo = try await apiUpdateGroup(groupId, groupProfile) + await MainActor.run { + chatModel.updateGroup(gInfo) + dismiss() + } + } catch let error { + let err = responseError(error) + saveGroupError = err + showSaveErrorAlert = true + logger.error("UserProfile apiUpdateProfile error: \(err)") + } + } + } +} + +struct GroupProfileView_Previews: PreviewProvider { + static var previews: some View { + GroupProfileView(groupId: 1, groupProfile: GroupProfile.sampleData) + } +} diff --git a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift index 0c6ee9c4f..46d92a671 100644 --- a/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/GroupChatInfoView.swift @@ -11,14 +11,15 @@ import SimpleXChat struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel - @ObservedObject var alertManager = AlertManager.shared + @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat var groupInfo: GroupInfo - @Binding var showSheet: Bool + @ObservedObject private var alertManager = AlertManager.shared @State private var members: [GroupMember] = [] @State private var alert: GroupChatInfoViewAlert? = nil @State private var showAddMembersSheet: Bool = false @State private var selectedMember: GroupMember? = nil + @State private var showGroupProfile: Bool = false enum GroupChatInfoViewAlert: Identifiable { case deleteGroupAlert @@ -34,7 +35,18 @@ struct GroupChatInfoView: View { groupInfoHeader() .listRowBackground(Color.clear) - Section(header: Text("\(members.count + 1) members")) { + Section { + Button { + showGroupProfile = true + } label: { + Label("Edit group profile", systemImage: "pencil") + } + } + .sheet(isPresented: $showGroupProfile) { + GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) + } + + Section("\(members.count + 1) members") { if (groupInfo.canAddMembers) { addMembersButton() } @@ -44,7 +56,7 @@ struct GroupChatInfoView: View { } } .sheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo, showSheet: $showAddMembersSheet) + AddGroupMembersView(chat: chat, groupInfo: groupInfo, membersToAdd: filterMembersToAdd(members)) } .sheet(item: $selectedMember) { member in GroupMemberInfoView(groupInfo: groupInfo, member: member) @@ -76,8 +88,10 @@ struct GroupChatInfoView: View { } } .task { - members = await apiListMembers(chat.chatInfo.apiId) - .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } // TODO owner first + let ms = await apiListMembers(chat.chatInfo.apiId) + await MainActor.run { + members = ms.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } + } } } @@ -178,7 +192,7 @@ struct GroupChatInfoView: View { try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) await MainActor.run { chatModel.removeChat(chat.chatInfo.id) - showSheet = false + dismiss() } } catch let error { logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)") @@ -196,9 +210,7 @@ struct GroupChatInfoView: View { primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) - await MainActor.run { - showSheet = false - } + await MainActor.run { dismiss() } } }, secondaryButton: .cancel() @@ -212,9 +224,7 @@ struct GroupChatInfoView: View { primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(chat.chatInfo.apiId) - await MainActor.run { - showSheet = false - } + await MainActor.run { dismiss() } } }, secondaryButton: .cancel() @@ -224,7 +234,6 @@ struct GroupChatInfoView: View { struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { - @State var showSheet = true - return GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData, showSheet: $showSheet) + GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 8d62603b9..05d62cb5e 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -146,7 +146,7 @@ struct UserProfile: View { } } } catch { - logger.error("UserProfile apiUpdateProfile error: \(error.localizedDescription)") + logger.error("UserProfile apiUpdateProfile error: \(responseError(error))") } editProfile = false } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index f5c34ec11..00c5bb01b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 5C9C2DA128929B6900CC63B1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9C28929B6900CC63B1 /* libgmp.a */; }; 5C9C2DA228929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9D28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a */; }; 5C9C2DA328929B6900CC63B1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9E28929B6900CC63B1 /* libgmpxx.a */; }; + 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; @@ -234,6 +235,7 @@ 5C9C2D9C28929B6900CC63B1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5C9C2D9D28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a"; sourceTree = ""; }; 5C9C2D9E28929B6900CC63B1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupProfileView.swift; sourceTree = ""; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; @@ -632,6 +634,7 @@ children = ( 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */, 647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */, + 5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */, ); path = Group; sourceTree = ""; @@ -891,6 +894,7 @@ 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */, 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */, 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */, + 5C9C2DA52894777E00CC63B1 /* GroupProfileView.swift in Sources */, 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme index 6a1d4192e..8b60dc4fa 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + enableAddressSanitizer = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 0d31ce6f8..d75701e92 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -40,6 +40,7 @@ public enum ChatCommand { case apiRemoveMember(groupId: Int64, memberId: Int64) case apiLeaveGroup(groupId: Int64) case apiListMembers(groupId: Int64) + case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) case getUserSMPServers case setUserSMPServers(smpServers: [String]) case apiSetNetworkConfig(networkConfig: NetCfg) @@ -101,6 +102,7 @@ public enum ChatCommand { case let .apiRemoveMember(groupId, memberId): return "/_remove #\(groupId) \(memberId)" case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" case let .apiListMembers(groupId): return "/_members #\(groupId)" + case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" @@ -162,6 +164,7 @@ public enum ChatCommand { case .apiRemoveMember: return "apiRemoveMember" case .apiLeaveGroup: return "apiLeaveGroup" case .apiListMembers: return "apiListMembers" + case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" case .apiSetNetworkConfig: return "apiSetNetworkConfig" @@ -270,6 +273,7 @@ public enum ChatResponse: Decodable, Error { case joinedGroupMember(groupInfo: GroupInfo, member: GroupMember) case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember) case groupRemoved(groupInfo: GroupInfo) + case groupUpdated(toGroup: GroupInfo) // receiving file events case rcvFileAccepted(chatItem: AChatItem) case rcvFileStart(chatItem: AChatItem) @@ -359,6 +363,7 @@ public enum ChatResponse: Decodable, Error { case .joinedGroupMember: return "joinedGroupMember" case .connectedToGroupMember: return "connectedToGroupMember" case .groupRemoved: return "groupRemoved" + case .groupUpdated: return "groupUpdated" case .rcvFileAccepted: return "rcvFileAccepted" case .rcvFileStart: return "rcvFileStart" case .rcvFileComplete: return "rcvFileComplete" @@ -449,6 +454,7 @@ public enum ChatResponse: Decodable, Error { case let .joinedGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" case let .connectedToGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)" case let .groupRemoved(groupInfo): return String(describing: groupInfo) + case let .groupUpdated(toGroup): return String(describing: toGroup) case let .rcvFileAccepted(chatItem): return String(describing: chatItem) case let .rcvFileStart(chatItem): return String(describing: chatItem) case let .rcvFileComplete(chatItem): return String(describing: chatItem) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 73f7325d7..f2e25890b 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -421,12 +421,17 @@ public enum ConnStatus: String, Decodable { public struct Group: Decodable { public var groupInfo: GroupInfo public var members: [GroupMember] + + public init(groupInfo: GroupInfo, members: [GroupMember]) { + self.groupInfo = groupInfo + self.members = members + } } public struct GroupInfo: Identifiable, Decodable, NamedChat { public var groupId: Int64 var localDisplayName: GroupName - var groupProfile: GroupProfile + public var groupProfile: GroupProfile public var membership: GroupMember var createdAt: Date var updatedAt: Date @@ -1261,6 +1266,7 @@ public enum RcvGroupEvent: Decodable { case memberDeleted(groupMemberId: Int64, profile: Profile) case userDeleted case groupDeleted + case groupUpdated(groupProfile: GroupProfile) var text: String { switch self { @@ -1272,6 +1278,7 @@ public enum RcvGroupEvent: Decodable { return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.displayNameWithOptionalFullName) case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item") case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item") + case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item") } } } @@ -1279,12 +1286,14 @@ public enum RcvGroupEvent: Decodable { public enum SndGroupEvent: Decodable { case memberDeleted(groupMemberId: Int64, profile: Profile) case userLeft + case groupUpdated(groupProfile: GroupProfile) var text: String { switch self { case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.displayNameWithOptionalFullName) case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item") + case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item") } } }