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:
committed by
GitHub
parent
f0f7226fa5
commit
05385ce997
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
64
apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
Normal file
64
apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift
Normal 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: "")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user