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>
This commit is contained in:
Evgeny Poberezkin
2022-10-01 10:57:18 +01:00
committed by GitHub
parent f0f7226fa5
commit 05385ce997
9 changed files with 221 additions and 25 deletions

View File

@@ -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<Int64, () -> 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 })

View File

@@ -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)

View File

@@ -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"

View File

@@ -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: "")
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}

View File

@@ -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 = "<group>"; };
5CE1330828E71B8F00FFFD8C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CE1330928E71B8F00FFFD8C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
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 = "<group>"; };
5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = SimpleXChat.docc; sourceTree = "<group>"; };
@@ -586,6 +588,7 @@
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */,
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */,
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */,
);
path = ChatList;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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

View File

@@ -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
)