diff --git a/apps/ios/LOCALIZATION.md b/apps/ios/LOCALIZATION.md
index f58b8918b..b0c6c7088 100644
--- a/apps/ios/LOCALIZATION.md
+++ b/apps/ios/LOCALIZATION.md
@@ -11,7 +11,7 @@ There are three ways XCode generates localization keys from strings:
3. All strings wrapped in `NSLocalizedString`. Please note that such strings do not support swift interpolation, instead formatted strings should be used:
```swift
-String.localizedStringWithFormat(NSLocalizedString("You can now send messages to %@", comment: "notification body")
+String.localizedStringWithFormat(NSLocalizedString("You can now send messages to %@", comment: "notification body"), value)
```
## Adding strings to the existing localizations
diff --git a/apps/ios/Shared/DebugJSON.playground/Contents.swift b/apps/ios/Shared/DebugJSON.playground/Contents.swift
new file mode 100644
index 000000000..e62ca1ab5
--- /dev/null
+++ b/apps/ios/Shared/DebugJSON.playground/Contents.swift
@@ -0,0 +1,20 @@
+import UIKit
+
+let s = """
+ {
+ "contactConnection" : {
+ "contactConnection" : {
+ "viaContactUri" : false,
+ "pccConnId" : 456,
+ "pccAgentConnId" : "cTdjbmR4ZzVzSmhEZHdzMQ==",
+ "pccConnStatus" : "new",
+ "updatedAt" : "2022-04-24T11:59:23.703162Z",
+ "createdAt" : "2022-04-24T11:59:23.703162Z"
+ }
+ }
+ }
+"""
+//let s = "\"2022-04-24T11:59:23.703162Z\""
+let json = getJSONDecoder()
+let d = s.data(using: .utf8)!
+print (try! json.decode(ChatInfo.self, from: d))
diff --git a/apps/ios/Shared/DebugJSON.playground/contents.xcplayground b/apps/ios/Shared/DebugJSON.playground/contents.xcplayground
new file mode 100644
index 000000000..cf026f228
--- /dev/null
+++ b/apps/ios/Shared/DebugJSON.playground/contents.xcplayground
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index 2a747881f..e2960a6ea 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -54,9 +54,16 @@ final class ChatModel: ObservableObject {
}
}
+ func updateContactConnection(_ contactConnection: PendingContactConnection) {
+ updateChat(.contactConnection(contactConnection: contactConnection))
+ }
+
func updateContact(_ contact: Contact) {
- let cInfo = ChatInfo.direct(contact: contact)
- if hasChat(contact.id) {
+ updateChat(.direct(contact: contact))
+ }
+
+ private func updateChat(_ cInfo: ChatInfo) {
+ if hasChat(cInfo.id) {
updateChatInfo(cInfo)
} else {
addChat(Chat(chatInfo: cInfo, chatItems: []))
@@ -248,6 +255,7 @@ enum ChatType: String {
case direct = "@"
case group = "#"
case contactRequest = "<@"
+ case contactConnection = ":"
}
protocol NamedChat {
@@ -268,6 +276,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case direct(contact: Contact)
case group(groupInfo: GroupInfo)
case contactRequest(contactRequest: UserContactRequest)
+ case contactConnection(contactConnection: PendingContactConnection)
var localDisplayName: String {
get {
@@ -275,6 +284,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.localDisplayName
case let .group(groupInfo): return groupInfo.localDisplayName
case let .contactRequest(contactRequest): return contactRequest.localDisplayName
+ case let .contactConnection(contactConnection): return contactConnection.localDisplayName
}
}
}
@@ -285,6 +295,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.displayName
case let .group(groupInfo): return groupInfo.displayName
case let .contactRequest(contactRequest): return contactRequest.displayName
+ case let .contactConnection(contactConnection): return contactConnection.displayName
}
}
}
@@ -295,6 +306,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.fullName
case let .group(groupInfo): return groupInfo.fullName
case let .contactRequest(contactRequest): return contactRequest.fullName
+ case let .contactConnection(contactConnection): return contactConnection.fullName
}
}
}
@@ -305,6 +317,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.image
case let .group(groupInfo): return groupInfo.image
case let .contactRequest(contactRequest): return contactRequest.image
+ case let .contactConnection(contactConnection): return contactConnection.image
}
}
}
@@ -315,6 +328,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.id
case let .group(groupInfo): return groupInfo.id
case let .contactRequest(contactRequest): return contactRequest.id
+ case let .contactConnection(contactConnection): return contactConnection.id
}
}
}
@@ -325,6 +339,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case .direct: return .direct
case .group: return .group
case .contactRequest: return .contactRequest
+ case .contactConnection: return .contactConnection
}
}
}
@@ -335,6 +350,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.apiId
case let .group(groupInfo): return groupInfo.apiId
case let .contactRequest(contactRequest): return contactRequest.apiId
+ case let .contactConnection(contactConnection): return contactConnection.apiId
}
}
}
@@ -345,6 +361,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.ready
case let .group(groupInfo): return groupInfo.ready
case let .contactRequest(contactRequest): return contactRequest.ready
+ case let .contactConnection(contactConnection): return contactConnection.ready
}
}
}
@@ -354,6 +371,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
case let .direct(contact): return contact.createdAt
case let .group(groupInfo): return groupInfo.createdAt
case let .contactRequest(contactRequest): return contactRequest.createdAt
+ case let .contactConnection(contactConnection): return contactConnection.createdAt
}
}
@@ -456,7 +474,7 @@ struct Contact: Identifiable, Decodable, NamedChat {
var id: ChatId { get { "@\(contactId)" } }
var apiId: Int64 { get { contactId } }
- var ready: Bool { get { activeConn.connStatus == "ready" } }
+ var ready: Bool { get { activeConn.connStatus == .ready } }
var displayName: String { get { profile.displayName } }
var fullName: String { get { profile.fullName } }
var image: String? { get { profile.image } }
@@ -476,9 +494,15 @@ struct ContactSubStatus: Decodable {
}
struct Connection: Decodable {
- var connStatus: String
+ var connId: Int64
+ var connStatus: ConnStatus
- static let sampleData = Connection(connStatus: "ready")
+ var id: ChatId { get { ":\(connId)" } }
+
+ static let sampleData = Connection(
+ connId: 1,
+ connStatus: .ready
+ )
}
struct UserContactRequest: Decodable, NamedChat {
@@ -486,6 +510,7 @@ struct UserContactRequest: Decodable, NamedChat {
var localDisplayName: ContactName
var profile: Profile
var createdAt: Date
+ var updatedAt: Date
var id: ChatId { get { "<@\(contactRequestId)" } }
var apiId: Int64 { get { contactRequestId } }
@@ -498,10 +523,91 @@ struct UserContactRequest: Decodable, NamedChat {
contactRequestId: 1,
localDisplayName: "alice",
profile: Profile.sampleData,
- createdAt: .now
+ createdAt: .now,
+ updatedAt: .now
)
}
+struct PendingContactConnection: Decodable, NamedChat {
+ var pccConnId: Int64
+ var pccAgentConnId: String
+ var pccConnStatus: ConnStatus
+ var viaContactUri: Bool
+ var createdAt: Date
+ var updatedAt: Date
+
+ var id: ChatId { get { ":\(pccConnId)" } }
+ var apiId: Int64 { get { pccConnId } }
+ var ready: Bool { get { false } }
+ var localDisplayName: String {
+ get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) }
+ }
+ var displayName: String {
+ get {
+ if let initiated = pccConnStatus.initiated {
+ return initiated && !viaContactUri
+ ? NSLocalizedString("invited to connect", comment: "chat list item title")
+ : NSLocalizedString("connecting…", comment: "chat list item title")
+ } else {
+ // this should not be in the list
+ return NSLocalizedString("connection established", comment: "chat list item title (it should not be shown")
+ }
+ }
+ }
+ var fullName: String { get { "" } }
+ var image: String? { get { nil } }
+ var initiated: Bool { get { (pccConnStatus.initiated ?? false) && !viaContactUri } }
+
+ var description: String {
+ get {
+ if let initiated = pccConnStatus.initiated {
+ return initiated && !viaContactUri
+ ? NSLocalizedString("you shared one-time link", comment: "chat list item description")
+ : viaContactUri
+ ? NSLocalizedString("via contact address link", comment: "chat list item description")
+ : NSLocalizedString("via one-time link", comment: "chat list item description")
+ } else {
+ return ""
+ }
+ }
+ }
+
+ static func getSampleData(_ status: ConnStatus = .new, viaContactUri: Bool = false) -> PendingContactConnection {
+ PendingContactConnection(
+ pccConnId: 1,
+ pccAgentConnId: "abcd",
+ pccConnStatus: status,
+ viaContactUri: viaContactUri,
+ createdAt: .now,
+ updatedAt: .now
+ )
+ }
+}
+
+enum ConnStatus: String, Decodable {
+ case new = "new"
+ case joined = "joined"
+ case requested = "requested"
+ case accepted = "accepted"
+ case sndReady = "snd-ready"
+ case ready = "ready"
+ case deleted = "deleted"
+
+ var initiated: Bool? {
+ get {
+ switch self {
+ case .new: return true
+ case .joined: return false
+ case .requested: return true
+ case .accepted: return true
+ case .sndReady: return false
+ case .ready: return nil
+ case .deleted: return nil
+ }
+ }
+ }
+}
+
struct GroupInfo: Identifiable, Decodable, NamedChat {
var groupId: Int64
var localDisplayName: GroupName
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index 524fd047b..af6803b79 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -52,7 +52,7 @@ enum ChatCommand {
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
case .startChat: return "/_start"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
- case .apiGetChats: return "/_get chats"
+ case .apiGetChats: return "/_get chats pcc=on"
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
case let .apiSendMessage(type, id, file, quotedItemId, mc):
switch (file, quotedItemId) {
@@ -174,6 +174,8 @@ enum ChatResponse: Decodable, Error {
case rcvFileAccepted
case rcvFileComplete(chatItem: AChatItem)
case ntfTokenStatus(status: NtfTknStatus)
+ case newContactConnection(connection: PendingContactConnection)
+ case contactConnectionDeleted(connection: PendingContactConnection)
case cmdOk
case chatCmdError(chatError: ChatError)
case chatError(chatError: ChatError)
@@ -220,6 +222,8 @@ enum ChatResponse: Decodable, Error {
case .rcvFileAccepted: return "rcvFileAccepted"
case .rcvFileComplete: return "rcvFileComplete"
case .ntfTokenStatus: return "ntfTokenStatus"
+ case .newContactConnection: return "newContactConnection"
+ case .contactConnectionDeleted: return "contactConnectionDeleted"
case .cmdOk: return "cmdOk"
case .chatCmdError: return "chatCmdError"
case .chatError: return "chatError"
@@ -269,6 +273,8 @@ enum ChatResponse: Decodable, Error {
case .rcvFileAccepted: return noDetails
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
case let .ntfTokenStatus(status): return String(describing: status)
+ case let .newContactConnection(connection): return String(describing: connection)
+ case let .contactConnectionDeleted(connection): return String(describing: connection)
case .cmdOk: return noDetails
case let .chatCmdError(chatError): return String(describing: chatError)
case let .chatError(chatError): return String(describing: chatError)
@@ -538,7 +544,8 @@ func apiConnect(connReq: String) async throws -> ConnReqType? {
func apiDeleteChat(type: ChatType, id: Int64) async throws {
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
- if case .contactDeleted = r { return }
+ if case .direct = type, case .contactDeleted = r { return }
+ if case .contactConnection = type, case .contactConnectionDeleted = r { return }
throw r
}
@@ -608,7 +615,7 @@ func acceptContactRequest(_ contactRequest: UserContactRequest) async {
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) }
} catch let error {
- logger.error("acceptContactRequest error: \(error.localizedDescription)")
+ logger.error("acceptContactRequest error: \(responseError(error))")
}
}
@@ -617,7 +624,7 @@ func rejectContactRequest(_ contactRequest: UserContactRequest) async {
try await apiRejectContactRequest(contactReqId: contactRequest.apiId)
DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) }
} catch let error {
- logger.error("rejectContactRequest: \(error.localizedDescription)")
+ logger.error("rejectContactRequest: \(responseError(error))")
}
}
@@ -629,7 +636,7 @@ func markChatRead(_ chat: Chat) async {
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo) }
} catch {
- logger.error("markChatRead apiChatRead error: \(error.localizedDescription)")
+ logger.error("markChatRead apiChatRead error: \(responseError(error))")
}
}
@@ -638,7 +645,7 @@ func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
DispatchQueue.main.async { ChatModel.shared.markChatItemRead(cInfo, cItem) }
} catch {
- logger.error("markChatItemRead apiChatRead error: \(error.localizedDescription)")
+ logger.error("markChatItemRead apiChatRead error: \(responseError(error))")
}
}
@@ -701,33 +708,37 @@ class ChatReceiver {
}
func processReceivedMsg(_ res: ChatResponse) {
- let chatModel = ChatModel.shared
+ let m = ChatModel.shared
DispatchQueue.main.async {
- chatModel.terminalItems.append(.resp(.now, res))
+ m.terminalItems.append(.resp(.now, res))
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
+ case let .newContactConnection(contactConnection):
+ m.updateContactConnection(contactConnection)
case let .contactConnected(contact):
- chatModel.updateContact(contact)
- chatModel.updateNetworkStatus(contact, .connected)
+ m.updateContact(contact)
+ m.removeChat(contact.activeConn.id)
+ m.updateNetworkStatus(contact, .connected)
NtfManager.shared.notifyContactConnected(contact)
case let .contactConnecting(contact):
- chatModel.updateContact(contact)
+ m.updateContact(contact)
+ m.removeChat(contact.activeConn.id)
case let .receivedContactRequest(contactRequest):
- chatModel.addChat(Chat(
+ m.addChat(Chat(
chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
chatItems: []
))
NtfManager.shared.notifyContactRequest(contactRequest)
case let .contactUpdated(toContact):
let cInfo = ChatInfo.direct(contact: toContact)
- if chatModel.hasChat(toContact.id) {
- chatModel.updateChatInfo(cInfo)
+ if m.hasChat(toContact.id) {
+ m.updateChatInfo(cInfo)
}
case let .contactSubscribed(contact):
processContactSubscribed(contact)
case let .contactDisconnected(contact):
- chatModel.updateContact(contact)
- chatModel.updateNetworkStatus(contact, .disconnected)
+ m.updateContact(contact)
+ m.updateNetworkStatus(contact, .disconnected)
case let .contactSubError(contact, chatError):
processContactSubError(contact, chatError)
case let .contactSubSummary(contactSubscriptions):
@@ -741,7 +752,7 @@ func processReceivedMsg(_ res: ChatResponse) {
case let .newChatItem(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
- chatModel.addChatItem(cInfo, cItem)
+ m.addChatItem(cInfo, cItem)
if let file = cItem.file,
file.fileSize <= maxImageSize {
Task {
@@ -758,11 +769,11 @@ func processReceivedMsg(_ res: ChatResponse) {
let cItem = aChatItem.chatItem
var res = false
if !cItem.isDeletedContent() {
- res = chatModel.upsertChatItem(cInfo, cItem)
+ res = m.upsertChatItem(cInfo, cItem)
}
if res {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
- } else if let endTask = chatModel.messageDelivery[cItem.id] {
+ } else if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndSent: endTask()
case .sndErrorAuth: endTask()
@@ -773,22 +784,22 @@ func processReceivedMsg(_ res: ChatResponse) {
case let .chatItemUpdated(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
- if chatModel.upsertChatItem(cInfo, cItem) {
+ if m.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
}
case let .chatItemDeleted(_, toChatItem):
let cInfo = toChatItem.chatInfo
let cItem = toChatItem.chatItem
if cItem.meta.itemDeleted {
- chatModel.removeChatItem(cInfo, cItem)
+ m.removeChatItem(cInfo, cItem)
} else {
// currently only broadcast deletion of rcv message can be received, and only this case should happen
- _ = chatModel.upsertChatItem(cInfo, cItem)
+ _ = m.upsertChatItem(cInfo, cItem)
}
case let .rcvFileComplete(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
- if chatModel.upsertChatItem(cInfo, cItem) {
+ if m.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
}
default:
diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
index bf20e1bd3..78b6777ea 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
@@ -21,6 +21,8 @@ struct ChatListNavLink: View {
groupNavLink(groupInfo)
case let .contactRequest(cReq):
contactRequestNavLink(cReq)
+ case let .contactConnection(cConn):
+ contactConnectionNavLink(cConn)
}
}
@@ -125,6 +127,31 @@ struct ChatListNavLink: View {
}
}
+ private func contactConnectionNavLink(_ contactConnection: PendingContactConnection) -> some View {
+ ContactConnectionView(contactConnection: contactConnection)
+ .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+ Button(role: .destructive) {
+ AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection))
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ .frame(height: 80)
+ .onTapGesture {
+ AlertManager.shared.showAlertMsg(
+ 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!"
+ )
+ }
+ }
+
private func deleteContactAlert(_ contact: Contact) -> Alert {
Alert(
title: Text("Delete contact?"),
@@ -137,7 +164,7 @@ struct ChatListNavLink: View {
chatModel.removeChat(contact.id)
}
} catch let error {
- logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
+ logger.error("ChatListNavLink.deleteContactAlert apiDeleteChat error: \(responseError(error))")
}
}
},
@@ -163,6 +190,29 @@ struct ChatListNavLink: View {
)
}
+ private func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection) -> Alert {
+ Alert(
+ title: Text("Delete pending connection?"),
+ message:
+ contactConnection.initiated
+ ? Text("The contact you shared this link with will NOT be able to connect!")
+ : Text("The connection you accepted will be cancelled!"),
+ primaryButton: .destructive(Text("Delete")) {
+ Task {
+ do {
+ try await apiDeleteChat(type: .contactConnection, id: contactConnection.apiId)
+ DispatchQueue.main.async {
+ chatModel.removeChat(contactConnection.id)
+ }
+ } catch let error {
+ logger.error("ChatListNavLink.deleteContactConnectionAlert apiDeleteChat error: \(responseError(error))")
+ }
+ }
+ },
+ secondaryButton: .cancel()
+ )
+ }
+
private func pendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert {
Alert(
title: Text("Contact is not connected yet!"),
diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift
index 20b513315..094f916bd 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -13,6 +13,7 @@ struct ChatListView: View {
// not really used in this view
@State private var showSettings = false
@State private var searchText = ""
+ @AppStorage("pendingConnections") private var pendingConnections = true
var user: User
@@ -64,9 +65,16 @@ struct ChatListView: View {
private func filteredChats() -> [Chat] {
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
- return s == ""
+ return s == "" && pendingConnections
? chatModel.chats
- : chatModel.chats.filter { $0.chatInfo.chatViewName.localizedLowercase.contains(s) }
+ : s == ""
+ ? chatModel.chats.filter {
+ pendingConnections || $0.chatInfo.chatType != .contactConnection
+ }
+ : chatModel.chats.filter {
+ (pendingConnections || $0.chatInfo.chatType != .contactConnection) &&
+ $0.chatInfo.chatViewName.localizedLowercase.contains(s)
+ }
}
private func connectViaUrlAlert(_ url: URL) -> Alert {
diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift
new file mode 100644
index 000000000..90c9b70bd
--- /dev/null
+++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift
@@ -0,0 +1,55 @@
+//
+// ContactConnectionView.swift
+// SimpleX (iOS)
+//
+// Created by Evgeny on 24/04/2022.
+// Copyright © 2022 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+
+struct ContactConnectionView: View {
+ var contactConnection: PendingContactConnection
+
+ 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)
+ VStack(alignment: .leading, spacing: 4) {
+ HStack(alignment: .top) {
+ Text(contactConnection.chatViewName)
+ .font(.title3)
+ .fontWeight(.bold)
+ .foregroundColor(.secondary)
+ .padding(.leading, 8)
+ .padding(.top, 4)
+ .frame(maxHeight: .infinity, alignment: .topLeading)
+ Spacer()
+ timestampText(contactConnection.updatedAt)
+ .font(.subheadline)
+ .padding(.trailing, 8)
+ .padding(.top, 4)
+ .frame(minWidth: 60, alignment: .trailing)
+ .foregroundColor(.secondary)
+ }
+ Text(contactConnection.description)
+ .frame(minHeight: 44, maxHeight: 44, alignment: .topLeading)
+ .padding([.leading, .trailing], 8)
+ .padding(.bottom, 4)
+ .padding(.top, 1)
+ }
+ }
+ }
+}
+
+struct ContactConnectionView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContactConnectionView(contactConnection: PendingContactConnection.getSampleData())
+ .previewLayout(.fixed(width: 360, height: 80))
+ }
+}
diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift
index 2eedbdeb4..31be17edf 100644
--- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift
+++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift
@@ -20,7 +20,7 @@ struct ContactRequestView: View {
.padding(.leading, 4)
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .top) {
- Text(ChatInfo.contactRequest(contactRequest: contactRequest).chatViewName)
+ Text(contactRequest.chatViewName)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.blue)
@@ -28,7 +28,7 @@ struct ContactRequestView: View {
.padding(.top, 4)
.frame(maxHeight: .infinity, alignment: .topLeading)
Spacer()
- timestampText(contactRequest.createdAt)
+ timestampText(contactRequest.updatedAt)
.font(.subheadline)
.padding(.trailing, 8)
.padding(.top, 4)
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index ff969c9ca..c2d329d10 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -18,7 +18,8 @@ struct SettingsView: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var chatModel: ChatModel
@Binding var showSettings: Bool
- @AppStorage("useNotifications") private var useNotifications: Bool = false
+ @AppStorage("useNotifications") private var useNotifications = false
+ @AppStorage("pendingConnections") private var pendingConnections = true
@State var showNotificationsAlert: Bool = false
@State var whichNotificationsAlert = NotificationAlert.enable
@@ -59,6 +60,11 @@ struct SettingsView: View {
}
Section("Settings") {
+ HStack {
+ Image(systemName: "link")
+ .padding(.trailing, 8)
+ Toggle("Show pending connections", isOn: $pendingConnections)
+ }
NavigationLink {
SMPServers()
.navigationTitle("Your SMP servers")
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 953e7abd5..9a6c0b7d2 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -12,6 +12,7 @@
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
+ 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
@@ -90,6 +91,8 @@
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; };
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; };
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; };
+ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = ""; };
+ 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = ""; };
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = ""; };
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; };
@@ -286,6 +289,7 @@
5CA059C5279559F40002BEB4 /* Assets.xcassets */,
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */,
5C764E7F279C7276000C6508 /* dummy.m */,
+ 5C13730C2815740A00F43030 /* DebugJSON.playground */,
);
path = Shared;
sourceTree = "";
@@ -342,6 +346,7 @@
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */,
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */,
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */,
+ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
);
path = ChatList;
sourceTree = "";
@@ -483,6 +488,7 @@
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
+ 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs
index a40773e9f..64321d7e6 100644
--- a/src/Simplex/Chat.hs
+++ b/src/Simplex/Chat.hs
@@ -365,8 +365,11 @@ processChatCommand = \case
unsetActive $ ActiveC localDisplayName
pure $ CRContactDeleted ct
gs -> throwChatError $ CEContactGroups ct gs
- CTContactConnection ->
- CRContactConnectionDeleted <$> withStore (\st -> deletePendingContactConnection st userId chatId)
+ CTContactConnection -> withChatLock . procCmd $ do
+ conn <- withStore $ \st -> getPendingContactConnection st userId chatId
+ withAgent $ \a -> deleteConnection a $ aConnId' conn
+ withStore $ \st -> deletePendingContactConnection st userId chatId
+ pure $ CRContactConnectionDeleted conn
CTGroup -> pure $ chatCmdError "not implemented"
CTContactRequest -> pure $ chatCmdError "not supported"
APIAcceptContact connReqId -> withUser $ \user@User {userId} -> withChatLock $ do
diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs
index 257fde225..9c381a776 100644
--- a/src/Simplex/Chat/Store.hs
+++ b/src/Simplex/Chat/Store.hs
@@ -152,6 +152,7 @@ module Simplex.Chat.Store
updateGroupChatItemsRead,
getSMPServers,
overwriteSMPServers,
+ getPendingContactConnection,
deletePendingContactConnection,
)
where
@@ -2727,21 +2728,39 @@ getContactConnectionChatPreviews_ db User {userId} _ =
stats = ChatStats {unreadCount = 0, minUnreadItemId = 0}
in AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
-deletePendingContactConnection :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m PendingContactConnection
+getPendingContactConnection :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m PendingContactConnection
+getPendingContactConnection st userId connId =
+ liftIOEither . withTransaction st $ \db -> do
+ firstRow toPendingContactConnection (SEPendingConnectionNotFound connId) $
+ DB.query
+ db
+ [sql|
+ SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, created_at, updated_at
+ FROM connections
+ WHERE user_id = ?
+ AND connection_id = ?
+ AND conn_type = ?
+ AND contact_id IS NULL
+ AND conn_level = 0
+ AND via_contact IS NULL
+ |]
+ (userId, connId, ConnContact)
+
+deletePendingContactConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> m ()
deletePendingContactConnection st userId connId =
- liftIOEither . withTransaction st $ \db -> runExceptT $ do
- conn <-
- ExceptT . firstRow toPendingContactConnection (SEPendingConnectionNotFound connId) $
- DB.query
- db
- [sql|
- SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, created_at, updated_at
- FROM connections
- WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL
- |]
- (userId, ConnContact)
- liftIO $ DB.execute db "DELETE FROM connections WHERE connection_id = ?" (Only connId)
- pure conn
+ liftIO . withTransaction st $ \db ->
+ DB.execute
+ db
+ [sql|
+ DELETE FROM connections
+ WHERE user_id = ?
+ AND connection_id = ?
+ AND conn_type = ?
+ AND contact_id IS NULL
+ AND conn_level = 0
+ AND via_contact IS NULL
+ |]
+ (userId, connId, ConnContact)
toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, UTCTime, UTCTime) -> PendingContactConnection
toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, createdAt, updatedAt) =
diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs
index e8f064c2d..fcb11f7f5 100644
--- a/src/Simplex/Chat/Types.hs
+++ b/src/Simplex/Chat/Types.hs
@@ -684,6 +684,9 @@ data PendingContactConnection = PendingContactConnection
}
deriving (Eq, Show, Generic)
+aConnId' :: PendingContactConnection -> ConnId
+aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId
+
instance ToJSON PendingContactConnection where toEncoding = J.genericToEncoding J.defaultOptions
data ConnStatus