From 05385ce997e3785bf3f6163815e87d7370298679 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 1 Oct 2022 10:57:18 +0100 Subject: [PATCH] ios: set alias on connection link, see link again, remove QR code on connection (#1155) * ios: set alias on connection link, see link again, remove QR code on connection * update UX for connection alias * change layout * layout * return pencil * incognito Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * color * style Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * fix * pencil color * update * remove UB sanitizer * exit edit mode * fix flicker Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- apps/ios/Shared/Model/ChatModel.swift | 11 ++ apps/ios/Shared/Model/SimpleXAPI.swift | 8 ++ .../Views/ChatList/ChatListNavLink.swift | 18 +-- .../ChatList/ContactConnectionInfo.swift | 64 +++++++++ .../ChatList/ContactConnectionView.swift | 125 +++++++++++++++--- .../Shared/Views/NewChat/CreateLinkView.swift | 4 + apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/APITypes.swift | 6 + apps/ios/SimpleXChat/ChatTypes.swift | 6 +- 9 files changed, 221 insertions(+), 25 deletions(-) create mode 100644 apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index ad22f27fb..897d9fba8 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -48,6 +48,8 @@ final class ChatModel: ObservableObject { @Published var activeCall: Call? @Published var callCommand: WCallCommand? @Published var showCallView = false + // currently showing QR code + @Published var connReqInv: String? var callWebView: WKWebView? var messageDelivery: Dictionary Void> = [:] @@ -326,6 +328,15 @@ final class ChatModel: ObservableObject { chats.insert(chat, at: position) } + func dismissConnReqView(_ id: String) { + if let connReqInv = connReqInv, + let c = getChat(id), + case let .contactConnection(contactConnection) = c.chatInfo, + connReqInv == contactConnection.connReqInv { + dismissAllSheets() + } + } + func removeChat(_ id: String) { withAnimation { chats.removeAll(where: { $0.id == id }) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 72aa16413..c7bbf684d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -468,6 +468,12 @@ func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Co throw r } +func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> PendingContactConnection? { + let r = await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias)) + if case let .connectionAliasUpdated(toConnection) = r { return toConnection } + throw r +} + func apiCreateUserAddress() async throws -> String { let r = await chatSendCmd(.createMyAddress) if case let .userContactLinkCreated(connReq) = r { return connReq } @@ -819,11 +825,13 @@ func processReceivedMsg(_ res: ChatResponse) async { m.removeChat(connection.id) case let .contactConnected(contact): m.updateContact(contact) + m.dismissConnReqView(contact.activeConn.id) m.removeChat(contact.activeConn.id) m.updateNetworkStatus(contact.id, .connected) NtfManager.shared.notifyContactConnected(contact) case let .contactConnecting(contact): m.updateContact(contact) + m.dismissConnReqView(contact.activeConn.id) m.removeChat(contact.activeConn.id) case let .receivedContactRequest(contactRequest): let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index aea9fda6e..c4d82252c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -211,15 +211,11 @@ struct ChatListNavLink: View { .frame(height: rowHeights[dynamicTypeSize]) .onTapGesture { AlertManager.shared.showAlertMsg( - title: - contactConnection.initiated - ? "You invited your contact" - : "You accepted connection", + title: contactConnection.initiated + ? "You invited your contact" + : "You accepted connection", // below are the same messages that are shown in alert - message: - contactConnection.viaContactUri - ? "You will be connected when your connection request is accepted, please wait or check later!" - : "You will be connected when your contact's device is online, please wait or check later!" + message: contactConnectionText(contactConnection) ) } } @@ -388,6 +384,12 @@ func joinGroup(_ groupId: Int64) { } } +func contactConnectionText(_ contactConnection: PendingContactConnection) -> LocalizedStringKey { + contactConnection.viaContactUri + ? "You will be connected when your connection request is accepted, please wait or check later!" + : "You will be connected when your contact's device is online, please wait or check later!" +} + struct ChatListNavLink_Previews: PreviewProvider { static var previews: some View { @State var chatId: String? = "@1" diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift new file mode 100644 index 000000000..3b7b899bf --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -0,0 +1,64 @@ +// +// ContactConnectionInfo.swift +// SimpleX (iOS) +// +// Created by Evgeny on 30/09/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContactConnectionInfo: View { + @EnvironmentObject var m: ChatModel + var contactConnection: PendingContactConnection + var connReqInvitation: String + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Text("Shared one-time link") + .font(.largeTitle) + .bold() + .padding(.vertical) + + HStack { + if contactConnection.incognito { + Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) + Spacer().frame(width: 8) + Text("A random profile will be sent to your contact").font(.footnote) + } else { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your chat profile will be sent to your contact").font(.footnote) + } + } + + Text(contactConnectionText(contactConnection)) + .padding(.top, 4) + .padding(.bottom, 8) + + QRCode(uri: connReqInvitation).padding(.bottom) + + Text("If you can't meet in person, **show QR code in the video call**, or share the link.") + .padding(.bottom) + Button { + showShareSheet(items: [connReqInvitation]) + } label: { + Label("Share invitation link", systemImage: "square.and.arrow.up") + } + .frame(maxWidth: .infinity, alignment: .center) + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .onAppear { m.connReqInv = connReqInvitation } + .onDisappear { m.connReqInv = nil } + } + } +} + +struct ContactConnectionInfo_Previews: PreviewProvider { + static var previews: some View { + ContactConnectionInfo(contactConnection: PendingContactConnection.getSampleData(), connReqInvitation: "") + } +} diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift index c93224429..59d8c5f40 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift @@ -10,30 +10,88 @@ import SwiftUI import SimpleXChat struct ContactConnectionView: View { - var contactConnection: PendingContactConnection - + @EnvironmentObject var m: ChatModel + @State var contactConnection: PendingContactConnection + @State private var editLocalAlias = false + @State private var localAlias = "" + @FocusState private var aliasTextFieldFocused: Bool + @State private var showContactConnectionInfo = false + var body: some View { HStack(spacing: 8) { - Image(systemName: contactConnection.initiated ? "link.badge.plus" : "link") - .resizable() - .foregroundColor(Color(uiColor: .secondarySystemBackground)) - .scaledToFill() - .frame(width: 48, height: 48) - .frame(width: 63, height: 63) - .padding(.leading, 4) + Group { + if contactConnection.initiated { + let v = Image(systemName: "qrcode") + .resizable() + .scaledToFill() + .frame(width: 40, height: 40) + if contactConnection.connReqInv == nil { + v.foregroundColor(Color(uiColor: .secondarySystemBackground)) + } else { + v.foregroundColor(contactConnection.incognito ? .indigo : .accentColor) + .onTapGesture { showContactConnectionInfo = true } + } + } else { + Image(systemName: "link") + .resizable() + .scaledToFill() + .frame(width: 48, height: 48) + .foregroundColor(Color(uiColor: .secondarySystemBackground)) + } + } + .frame(width: 63, height: 63) + .padding(.leading, 4) + VStack(alignment: .leading, spacing: 0) { HStack(alignment: .top) { - Text(contactConnection.chatViewName) - .font(.title3) - .fontWeight(.bold) + Image(systemName: "pencil") + .resizable() + .scaledToFill() + .frame(width: 16, height: 16) .foregroundColor(.secondary) .padding(.leading, 8) - .frame(alignment: .topLeading) + .padding(.top, 8) + .onTapGesture(perform: enableEditing) + + if editLocalAlias { + let v = TextField("Set contact name…", text: $localAlias) + .font(.title3) + .disableAutocorrection(true) + .focused($aliasTextFieldFocused) + .submitLabel(.done) + .onSubmit(setConnectionAlias) + .foregroundColor(.secondary) + .padding(.trailing, 8) + .onTapGesture {} + .onChange(of: aliasTextFieldFocused) { focussed in + if !focussed { + editLocalAlias = false + } + } + if #available(iOS 16.0, *) { + v.bold() + } else { + v + } + } else { + Text(contactConnection.chatViewName) + .font(.title3) + .bold() + .allowsTightening(false) + .foregroundColor(.secondary) + .padding(.trailing, 8) + .padding(.top, 1) + .padding(.bottom, 0.5) + .frame(alignment: .topLeading) + .onTapGesture(perform: enableEditing) + } + Spacer() + formatTimestampText(contactConnection.updatedAt) .font(.subheadline) .padding(.trailing, 8) - .padding(.top, 4) + .padding(.vertical, 4) .frame(minWidth: 60, alignment: .trailing) .foregroundColor(.secondary) } @@ -41,11 +99,48 @@ struct ContactConnectionView: View { Text(contactConnection.description) .frame(alignment: .topLeading) - .padding([.leading, .trailing], 8) + .padding(.horizontal, 8) + .padding(.bottom, 2) Spacer() } .frame(maxHeight: .infinity) + .sheet(isPresented: $showContactConnectionInfo) { + if let connReqInv = contactConnection.connReqInv { + ContactConnectionInfo(contactConnection: contactConnection, connReqInvitation: connReqInv) + } + } + } + } + + private func enableEditing() { + editLocalAlias = true + aliasTextFieldFocused = true + localAlias = contactConnection.localAlias + } + + private func setConnectionAlias() { + if localAlias == contactConnection.localAlias { + aliasTextFieldFocused = false + editLocalAlias = false + return + } + Task { + let prevAlias = contactConnection.localAlias + contactConnection.localAlias = localAlias + do { + if let conn = try await apiSetConnectionAlias(connId: contactConnection.pccConnId, localAlias: localAlias) { + await MainActor.run { + contactConnection = conn + ChatModel.shared.updateContactConnection(conn) + aliasTextFieldFocused = false + editLocalAlias = false + } + } + } catch { + logger.error("setContactAlias error: \(responseError(error))") + contactConnection.localAlias = prevAlias + } } } } diff --git a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift index 488e8727f..03700a519 100644 --- a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift +++ b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift @@ -14,6 +14,7 @@ enum CreateLinkTab { } struct CreateLinkView: View { + @EnvironmentObject var m: ChatModel @State var selection: CreateLinkTab @State var connReqInvitation: String = "" @State private var creatingConnReq = false @@ -42,6 +43,8 @@ struct CreateLinkView: View { createInvitation() } } + .onAppear { m.connReqInv = connReqInvitation } + .onDisappear { m.connReqInv = nil } } private func createInvitation() { @@ -51,6 +54,7 @@ struct CreateLinkView: View { await MainActor.run { if let connReq = connReq { connReqInvitation = connReq + m.connReqInv = connReq } else { creatingConnReq = false } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 75cda600f..0db3480d7 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ 5CE1330C28E71B8F00FFFD8C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330728E71B8F00FFFD8C /* libgmp.a */; }; 5CE1330D28E71B8F00FFFD8C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330828E71B8F00FFFD8C /* libffi.a */; }; 5CE1330E28E71B8F00FFFD8C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330928E71B8F00FFFD8C /* libgmpxx.a */; }; + 5CE1331028E7391000FFFD8C /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; }; @@ -304,6 +305,7 @@ 5CE1330728E71B8F00FFFD8C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5CE1330828E71B8F00FFFD8C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CE1330928E71B8F00FFFD8C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SimpleXChat.h; sourceTree = ""; }; 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SimpleXChat.docc; sourceTree = ""; }; @@ -586,6 +588,7 @@ 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */, 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */, 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, + 5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */, ); path = ChatList; sourceTree = ""; @@ -905,6 +908,7 @@ 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */, 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, + 5CE1331028E7391000FFFD8C /* ContactConnectionInfo.swift in Sources */, 5CB2085128DB64CA00D024EC /* CreateLinkView.swift in Sources */, 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */, 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 8aafcbc90..d794f785d 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -57,6 +57,7 @@ public enum ChatCommand { case listContacts case apiUpdateProfile(profile: Profile) case apiSetContactAlias(contactId: Int64, localAlias: String) + case apiSetConnectionAlias(connId: Int64, localAlias: String) case createMyAddress case deleteMyAddress case showMyAddress @@ -124,6 +125,7 @@ public enum ChatCommand { case .listContacts: return "/contacts" case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))" case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))" case .createMyAddress: return "/address" case .deleteMyAddress: return "/delete_address" case .showMyAddress: return "/show_address" @@ -190,6 +192,7 @@ public enum ChatCommand { case .listContacts: return "listContacts" case .apiUpdateProfile: return "apiUpdateProfile" case .apiSetContactAlias: return "apiSetContactAlias" + case .apiSetConnectionAlias: return "apiSetConnectionAlias" case .createMyAddress: return "createMyAddress" case .deleteMyAddress: return "deleteMyAddress" case .showMyAddress: return "showMyAddress" @@ -257,6 +260,7 @@ public enum ChatResponse: Decodable, Error { case userProfileNoChange case userProfileUpdated(fromProfile: Profile, toProfile: Profile) case contactAliasUpdated(toContact: Contact) + case connectionAliasUpdated(toConnection: PendingContactConnection) case userContactLink(connReqContact: String) case userContactLinkCreated(connReqContact: String) case userContactLinkDeleted @@ -350,6 +354,7 @@ public enum ChatResponse: Decodable, Error { case .userProfileNoChange: return "userProfileNoChange" case .userProfileUpdated: return "userProfileUpdated" case .contactAliasUpdated: return "contactAliasUpdated" + case .connectionAliasUpdated: return "connectionAliasUpdated" case .userContactLink: return "userContactLink" case .userContactLinkCreated: return "userContactLinkCreated" case .userContactLinkDeleted: return "userContactLinkDeleted" @@ -443,6 +448,7 @@ public enum ChatResponse: Decodable, Error { case .userProfileNoChange: return noDetails case let .userProfileUpdated(_, toProfile): return String(describing: toProfile) case let .contactAliasUpdated(toContact): return String(describing: toContact) + case let .connectionAliasUpdated(toConnection): return String(describing: toConnection) case let .userContactLink(connReq): return connReq case let .userContactLinkCreated(connReq): return connReq case .userContactLinkDeleted: return noDetails diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index f946f2139..01632b302 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -419,11 +419,13 @@ public struct UserContactRequest: Decodable, NamedChat { } public struct PendingContactConnection: Decodable, NamedChat { - var pccConnId: Int64 + public var pccConnId: Int64 var pccAgentConnId: String var pccConnStatus: ConnStatus public var viaContactUri: Bool public var customUserProfileId: Int64? + public var connReqInv: String? + public var localAlias: String var createdAt: Date public var updatedAt: Date @@ -448,7 +450,6 @@ public struct PendingContactConnection: Decodable, NamedChat { } public var fullName: String { get { "" } } public var image: String? { get { nil } } - public var localAlias: String { "" } public var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } } public var incognito: Bool { @@ -491,6 +492,7 @@ public struct PendingContactConnection: Decodable, NamedChat { pccAgentConnId: "abcd", pccConnStatus: status, viaContactUri: viaContactUri, + localAlias: "", createdAt: .now, updatedAt: .now )