diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index af544a3692..a1dd0eeb96 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -559,8 +559,8 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws { throw r } -func apiNewGroup(_ gp: GroupProfile) throws -> GroupInfo { - let r = chatSendCmdSync(.newGroup(groupProfile: gp)) +func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo { + let r = chatSendCmdSync(.newGroup(groupProfile: p)) if case let .groupCreated(groupInfo) = r { return groupInfo } throw r } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 92a9de395b..957c489f49 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -12,25 +12,50 @@ import SimpleXChat struct AddGroupView: View { @Binding var openedSheet: NewChatAction? @EnvironmentObject var m: ChatModel - @State private var displayName: String = "" - @State private var fullName: String = "" + @State private var profile = GroupProfile(displayName: "", fullName: "") @FocusState private var focusDisplayName @FocusState private var focusFullName + @State private var showChooseSource = false + @State private var showImagePicker = false + @State private var showTakePhoto = false + @State private var chosenImage: UIImage? = nil var body: some View { VStack(alignment: .leading) { - Text("Create group") + Text("Create secret group") .font(.largeTitle) + .padding(.vertical, 4) + Text("The group is fully decentralized – it is visible only to the members.") .padding(.bottom, 4) - Text("Enter group details") .padding(.bottom) + + ZStack(alignment: .center) { + ZStack(alignment: .topTrailing) { + profileImageView(profile.image) + if profile.image != nil { + Button { + profile.image = nil + } label: { + Image(systemName: "multiply") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 12) + } + } + } + + editImageButton { showChooseSource = true } + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.bottom, 4) + ZStack(alignment: .topLeading) { - if !validDisplayName(displayName) { + if !validDisplayName(profile.displayName) { Image(systemName: "exclamationmark.circle") .foregroundColor(.red) .padding(.top, 4) } - textField("Display name", text: $displayName) + textField("Group display name", text: $profile.displayName) .focused($focusDisplayName) .submitLabel(.next) .onSubmit { @@ -38,7 +63,7 @@ struct AddGroupView: View { else { focusDisplayName = true } } } - textField("Full name (optional)", text: $fullName) + textField("Group full name (optional)", text: $profile.fullName) .focused($focusFullName) .submitLabel(.go) .onSubmit { @@ -58,9 +83,39 @@ struct AddGroupView: View { .frame(maxWidth: .infinity, alignment: .trailing) } .onAppear() { - focusDisplayName = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + focusDisplayName = true + } } .padding() + .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 { + profile.image = resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500) + } else { + profile.image = nil + } + } + .contentShape(Rectangle()) + .onTapGesture { hideKeyboard() } } func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { @@ -73,12 +128,8 @@ struct AddGroupView: View { func createGroup() { hideKeyboard() - let groupProfile = GroupProfile( - displayName: displayName, - fullName: fullName - ) do { - let groupInfo = try apiNewGroup(groupProfile) + let groupInfo = try apiNewGroup(profile) m.addChat(Chat(chatInfo: .group(groupInfo: groupInfo), chatItems: [])) openedSheet = nil DispatchQueue.main.async { @@ -88,7 +139,7 @@ struct AddGroupView: View { openedSheet = nil AlertManager.shared.showAlert( Alert( - title: Text("Failed to create group"), + title: Text("Error creating group"), message: Text(responseError(error)) ) ) @@ -100,7 +151,7 @@ struct AddGroupView: View { } func canCreateProfile() -> Bool { - displayName != "" && validDisplayName(displayName) + profile.displayName != "" && validDisplayName(profile.displayName) } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index 19a88ca3c4..74444fa01f 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -34,7 +34,7 @@ struct NewChatButton: View { 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 secret group") { actionSheet = .createGroup } } .sheet(item: $actionSheet) { sheet in switch sheet { diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 8b2fcd627e..ef92515151 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -113,7 +113,7 @@ struct CreateProfile: View { } func validDisplayName(_ name: String) -> Bool { - name.firstIndex(of: " ") == nil + name.firstIndex(of: " ") == nil && name.first != "@" && name.first != "#" } struct CreateProfile_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 52137f476c..8d62603b93 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -130,23 +130,6 @@ struct UserProfile: View { .padding(.bottom) } - func profileImageView(_ imageStr: String?) -> some View { - ProfileImage(imageStr: imageStr) - .aspectRatio(1, contentMode: .fit) - .frame(maxWidth: 192, maxHeight: 192) - } - - func editImageButton(action: @escaping () -> Void) -> some View { - Button { - action() - } label: { - Image(systemName: "camera") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 48) - } - } - func startEditingImage(_ user: User) { profile = user.profile editProfile = true @@ -170,6 +153,23 @@ struct UserProfile: View { } } +func profileImageView(_ imageStr: String?) -> some View { + ProfileImage(imageStr: imageStr) + .aspectRatio(1, contentMode: .fit) + .frame(maxWidth: 192, maxHeight: 192) +} + +func editImageButton(action: @escaping () -> Void) -> some View { + Button { + action() + } label: { + Image(systemName: "camera") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 48) + } +} + struct UserProfile_Previews: PreviewProvider { static var previews: some View { let chatModel1 = ChatModel() diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index d567324142..f5c34ec115 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -50,6 +50,11 @@ 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; }; + 5C9C2D9F28929B6900CC63B1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9A28929B6900CC63B1 /* libffi.a */; }; + 5C9C2DA028929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9C2D9B28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a */; }; + 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 */; }; 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 */; }; @@ -57,11 +62,6 @@ 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */; }; 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; }; 5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; - 5CAB35F828870432009BAA9E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAB35F328870432009BAA9E /* libgmpxx.a */; }; - 5CAB35F928870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAB35F428870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF-ghc8.10.7.a */; }; - 5CAB35FA28870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAB35F528870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF.a */; }; - 5CAB35FB28870432009BAA9E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAB35F628870432009BAA9E /* libffi.a */; }; - 5CAB35FC28870432009BAA9E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAB35F728870432009BAA9E /* libgmp.a */; }; 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; }; 5CB0BA8B2826CB3A00B3292C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA892826CB3A00B3292C /* Localizable.strings */; }; 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8D2827126500B3292C /* OnboardingView.swift */; }; @@ -229,6 +229,11 @@ 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = ""; }; + 5C9C2D9A28929B6900CC63B1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C9C2D9B28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a"; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -239,11 +244,6 @@ 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = ""; }; - 5CAB35F328870432009BAA9E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CAB35F428870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF-ghc8.10.7.a"; sourceTree = ""; }; - 5CAB35F528870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF.a"; sourceTree = ""; }; - 5CAB35F628870432009BAA9E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CAB35F728870432009BAA9E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5CB0BA872826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 5CB0BA8A2826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CB0BA8D2827126500B3292C /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; @@ -337,13 +337,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CAB35F828870432009BAA9E /* libgmpxx.a in Frameworks */, - 5CAB35FC28870432009BAA9E /* libgmp.a in Frameworks */, - 5CAB35FB28870432009BAA9E /* libffi.a in Frameworks */, + 5C9C2DA028929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a in Frameworks */, + 5C9C2DA128929B6900CC63B1 /* libgmp.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CAB35FA28870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF.a in Frameworks */, + 5C9C2DA228929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a in Frameworks */, + 5C9C2D9F28929B6900CC63B1 /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CAB35F928870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF-ghc8.10.7.a in Frameworks */, + 5C9C2DA328929B6900CC63B1 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -399,11 +399,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CAB35F628870432009BAA9E /* libffi.a */, - 5CAB35F728870432009BAA9E /* libgmp.a */, - 5CAB35F328870432009BAA9E /* libgmpxx.a */, - 5CAB35F428870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF-ghc8.10.7.a */, - 5CAB35F528870432009BAA9E /* libHSsimplex-chat-3.0.0-LLJFvekvxJh78JPfTQjcLF.a */, + 5C9C2D9A28929B6900CC63B1 /* libffi.a */, + 5C9C2D9C28929B6900CC63B1 /* libgmp.a */, + 5C9C2D9E28929B6900CC63B1 /* libgmpxx.a */, + 5C9C2D9D28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw-ghc8.10.7.a */, + 5C9C2D9B28929B6900CC63B1 /* libHSsimplex-chat-3.1.0-FNUbBjLYHjnDjt6ldpTolw.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 6d96baf178..0d31ce6f89 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -95,7 +95,7 @@ public enum ChatCommand { case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)" case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)" - case let .newGroup(groupProfile): return "/group \(groupProfile.displayName) \(groupProfile.fullName)" + case let .newGroup(groupProfile): return "/_group \(encodeJSON(groupProfile))" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" case let .apiRemoveMember(groupId, memberId): return "/_remove #\(groupId) \(memberId)"