mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
ios: pending contact connections UI, core: delete connections on the server when deleting in UI/db (#565)
* ios: started pending connections UI * ios: UI for pending contact connections complete * this has to be getter, or it would break JSON parsing * ios: update "initiated" status of connection
This commit is contained in:
parent
db4731f19b
commit
89c36d42e2
14 changed files with 343 additions and 52 deletions
|
@ -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:
|
3. All strings wrapped in `NSLocalizedString`. Please note that such strings do not support swift interpolation, instead formatted strings should be used:
|
||||||
|
|
||||||
```swift
|
```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
|
## Adding strings to the existing localizations
|
||||||
|
|
20
apps/ios/Shared/DebugJSON.playground/Contents.swift
Normal file
20
apps/ios/Shared/DebugJSON.playground/Contents.swift
Normal file
|
@ -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))
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'>
|
||||||
|
<timeline fileName='timeline.xctimeline'/>
|
||||||
|
</playground>
|
|
@ -54,9 +54,16 @@ final class ChatModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateContactConnection(_ contactConnection: PendingContactConnection) {
|
||||||
|
updateChat(.contactConnection(contactConnection: contactConnection))
|
||||||
|
}
|
||||||
|
|
||||||
func updateContact(_ contact: Contact) {
|
func updateContact(_ contact: Contact) {
|
||||||
let cInfo = ChatInfo.direct(contact: contact)
|
updateChat(.direct(contact: contact))
|
||||||
if hasChat(contact.id) {
|
}
|
||||||
|
|
||||||
|
private func updateChat(_ cInfo: ChatInfo) {
|
||||||
|
if hasChat(cInfo.id) {
|
||||||
updateChatInfo(cInfo)
|
updateChatInfo(cInfo)
|
||||||
} else {
|
} else {
|
||||||
addChat(Chat(chatInfo: cInfo, chatItems: []))
|
addChat(Chat(chatInfo: cInfo, chatItems: []))
|
||||||
|
@ -248,6 +255,7 @@ enum ChatType: String {
|
||||||
case direct = "@"
|
case direct = "@"
|
||||||
case group = "#"
|
case group = "#"
|
||||||
case contactRequest = "<@"
|
case contactRequest = "<@"
|
||||||
|
case contactConnection = ":"
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol NamedChat {
|
protocol NamedChat {
|
||||||
|
@ -268,6 +276,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||||
case direct(contact: Contact)
|
case direct(contact: Contact)
|
||||||
case group(groupInfo: GroupInfo)
|
case group(groupInfo: GroupInfo)
|
||||||
case contactRequest(contactRequest: UserContactRequest)
|
case contactRequest(contactRequest: UserContactRequest)
|
||||||
|
case contactConnection(contactConnection: PendingContactConnection)
|
||||||
|
|
||||||
var localDisplayName: String {
|
var localDisplayName: String {
|
||||||
get {
|
get {
|
||||||
|
@ -275,6 +284,7 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||||
case let .direct(contact): return contact.localDisplayName
|
case let .direct(contact): return contact.localDisplayName
|
||||||
case let .group(groupInfo): return groupInfo.localDisplayName
|
case let .group(groupInfo): return groupInfo.localDisplayName
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct(contact): return contact.displayName
|
||||||
case let .group(groupInfo): return groupInfo.displayName
|
case let .group(groupInfo): return groupInfo.displayName
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct(contact): return contact.fullName
|
||||||
case let .group(groupInfo): return groupInfo.fullName
|
case let .group(groupInfo): return groupInfo.fullName
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct(contact): return contact.image
|
||||||
case let .group(groupInfo): return groupInfo.image
|
case let .group(groupInfo): return groupInfo.image
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct(contact): return contact.id
|
||||||
case let .group(groupInfo): return groupInfo.id
|
case let .group(groupInfo): return groupInfo.id
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct: return .direct
|
||||||
case .group: return .group
|
case .group: return .group
|
||||||
case .contactRequest: return .contactRequest
|
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 .direct(contact): return contact.apiId
|
||||||
case let .group(groupInfo): return groupInfo.apiId
|
case let .group(groupInfo): return groupInfo.apiId
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct(contact): return contact.ready
|
||||||
case let .group(groupInfo): return groupInfo.ready
|
case let .group(groupInfo): return groupInfo.ready
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 .direct(contact): return contact.createdAt
|
||||||
case let .group(groupInfo): return groupInfo.createdAt
|
case let .group(groupInfo): return groupInfo.createdAt
|
||||||
case let .contactRequest(contactRequest): return contactRequest.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 id: ChatId { get { "@\(contactId)" } }
|
||||||
var apiId: Int64 { 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 displayName: String { get { profile.displayName } }
|
||||||
var fullName: String { get { profile.fullName } }
|
var fullName: String { get { profile.fullName } }
|
||||||
var image: String? { get { profile.image } }
|
var image: String? { get { profile.image } }
|
||||||
|
@ -476,9 +494,15 @@ struct ContactSubStatus: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Connection: 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 {
|
struct UserContactRequest: Decodable, NamedChat {
|
||||||
|
@ -486,6 +510,7 @@ struct UserContactRequest: Decodable, NamedChat {
|
||||||
var localDisplayName: ContactName
|
var localDisplayName: ContactName
|
||||||
var profile: Profile
|
var profile: Profile
|
||||||
var createdAt: Date
|
var createdAt: Date
|
||||||
|
var updatedAt: Date
|
||||||
|
|
||||||
var id: ChatId { get { "<@\(contactRequestId)" } }
|
var id: ChatId { get { "<@\(contactRequestId)" } }
|
||||||
var apiId: Int64 { get { contactRequestId } }
|
var apiId: Int64 { get { contactRequestId } }
|
||||||
|
@ -498,10 +523,91 @@ struct UserContactRequest: Decodable, NamedChat {
|
||||||
contactRequestId: 1,
|
contactRequestId: 1,
|
||||||
localDisplayName: "alice",
|
localDisplayName: "alice",
|
||||||
profile: Profile.sampleData,
|
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 {
|
struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||||
var groupId: Int64
|
var groupId: Int64
|
||||||
var localDisplayName: GroupName
|
var localDisplayName: GroupName
|
||||||
|
|
|
@ -52,7 +52,7 @@ enum ChatCommand {
|
||||||
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
|
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
|
||||||
case .startChat: return "/_start"
|
case .startChat: return "/_start"
|
||||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
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 .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
|
||||||
case let .apiSendMessage(type, id, file, quotedItemId, mc):
|
case let .apiSendMessage(type, id, file, quotedItemId, mc):
|
||||||
switch (file, quotedItemId) {
|
switch (file, quotedItemId) {
|
||||||
|
@ -174,6 +174,8 @@ enum ChatResponse: Decodable, Error {
|
||||||
case rcvFileAccepted
|
case rcvFileAccepted
|
||||||
case rcvFileComplete(chatItem: AChatItem)
|
case rcvFileComplete(chatItem: AChatItem)
|
||||||
case ntfTokenStatus(status: NtfTknStatus)
|
case ntfTokenStatus(status: NtfTknStatus)
|
||||||
|
case newContactConnection(connection: PendingContactConnection)
|
||||||
|
case contactConnectionDeleted(connection: PendingContactConnection)
|
||||||
case cmdOk
|
case cmdOk
|
||||||
case chatCmdError(chatError: ChatError)
|
case chatCmdError(chatError: ChatError)
|
||||||
case chatError(chatError: ChatError)
|
case chatError(chatError: ChatError)
|
||||||
|
@ -220,6 +222,8 @@ enum ChatResponse: Decodable, Error {
|
||||||
case .rcvFileAccepted: return "rcvFileAccepted"
|
case .rcvFileAccepted: return "rcvFileAccepted"
|
||||||
case .rcvFileComplete: return "rcvFileComplete"
|
case .rcvFileComplete: return "rcvFileComplete"
|
||||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||||
|
case .newContactConnection: return "newContactConnection"
|
||||||
|
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||||
case .cmdOk: return "cmdOk"
|
case .cmdOk: return "cmdOk"
|
||||||
case .chatCmdError: return "chatCmdError"
|
case .chatCmdError: return "chatCmdError"
|
||||||
case .chatError: return "chatError"
|
case .chatError: return "chatError"
|
||||||
|
@ -269,6 +273,8 @@ enum ChatResponse: Decodable, Error {
|
||||||
case .rcvFileAccepted: return noDetails
|
case .rcvFileAccepted: return noDetails
|
||||||
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
|
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
|
||||||
case let .ntfTokenStatus(status): return String(describing: status)
|
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 .cmdOk: return noDetails
|
||||||
case let .chatCmdError(chatError): return String(describing: chatError)
|
case let .chatCmdError(chatError): return String(describing: chatError)
|
||||||
case let .chatError(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 {
|
func apiDeleteChat(type: ChatType, id: Int64) async throws {
|
||||||
let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false)
|
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
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,7 +615,7 @@ func acceptContactRequest(_ contactRequest: UserContactRequest) async {
|
||||||
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
|
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
|
||||||
DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) }
|
DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) }
|
||||||
} catch let error {
|
} 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)
|
try await apiRejectContactRequest(contactReqId: contactRequest.apiId)
|
||||||
DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) }
|
DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) }
|
||||||
} catch let error {
|
} 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)
|
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
||||||
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo) }
|
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo) }
|
||||||
} catch {
|
} 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))
|
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
|
||||||
DispatchQueue.main.async { ChatModel.shared.markChatItemRead(cInfo, cItem) }
|
DispatchQueue.main.async { ChatModel.shared.markChatItemRead(cInfo, cItem) }
|
||||||
} catch {
|
} 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) {
|
func processReceivedMsg(_ res: ChatResponse) {
|
||||||
let chatModel = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
chatModel.terminalItems.append(.resp(.now, res))
|
m.terminalItems.append(.resp(.now, res))
|
||||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||||
switch res {
|
switch res {
|
||||||
|
case let .newContactConnection(contactConnection):
|
||||||
|
m.updateContactConnection(contactConnection)
|
||||||
case let .contactConnected(contact):
|
case let .contactConnected(contact):
|
||||||
chatModel.updateContact(contact)
|
m.updateContact(contact)
|
||||||
chatModel.updateNetworkStatus(contact, .connected)
|
m.removeChat(contact.activeConn.id)
|
||||||
|
m.updateNetworkStatus(contact, .connected)
|
||||||
NtfManager.shared.notifyContactConnected(contact)
|
NtfManager.shared.notifyContactConnected(contact)
|
||||||
case let .contactConnecting(contact):
|
case let .contactConnecting(contact):
|
||||||
chatModel.updateContact(contact)
|
m.updateContact(contact)
|
||||||
|
m.removeChat(contact.activeConn.id)
|
||||||
case let .receivedContactRequest(contactRequest):
|
case let .receivedContactRequest(contactRequest):
|
||||||
chatModel.addChat(Chat(
|
m.addChat(Chat(
|
||||||
chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
|
chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
|
||||||
chatItems: []
|
chatItems: []
|
||||||
))
|
))
|
||||||
NtfManager.shared.notifyContactRequest(contactRequest)
|
NtfManager.shared.notifyContactRequest(contactRequest)
|
||||||
case let .contactUpdated(toContact):
|
case let .contactUpdated(toContact):
|
||||||
let cInfo = ChatInfo.direct(contact: toContact)
|
let cInfo = ChatInfo.direct(contact: toContact)
|
||||||
if chatModel.hasChat(toContact.id) {
|
if m.hasChat(toContact.id) {
|
||||||
chatModel.updateChatInfo(cInfo)
|
m.updateChatInfo(cInfo)
|
||||||
}
|
}
|
||||||
case let .contactSubscribed(contact):
|
case let .contactSubscribed(contact):
|
||||||
processContactSubscribed(contact)
|
processContactSubscribed(contact)
|
||||||
case let .contactDisconnected(contact):
|
case let .contactDisconnected(contact):
|
||||||
chatModel.updateContact(contact)
|
m.updateContact(contact)
|
||||||
chatModel.updateNetworkStatus(contact, .disconnected)
|
m.updateNetworkStatus(contact, .disconnected)
|
||||||
case let .contactSubError(contact, chatError):
|
case let .contactSubError(contact, chatError):
|
||||||
processContactSubError(contact, chatError)
|
processContactSubError(contact, chatError)
|
||||||
case let .contactSubSummary(contactSubscriptions):
|
case let .contactSubSummary(contactSubscriptions):
|
||||||
|
@ -741,7 +752,7 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||||
case let .newChatItem(aChatItem):
|
case let .newChatItem(aChatItem):
|
||||||
let cInfo = aChatItem.chatInfo
|
let cInfo = aChatItem.chatInfo
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
chatModel.addChatItem(cInfo, cItem)
|
m.addChatItem(cInfo, cItem)
|
||||||
if let file = cItem.file,
|
if let file = cItem.file,
|
||||||
file.fileSize <= maxImageSize {
|
file.fileSize <= maxImageSize {
|
||||||
Task {
|
Task {
|
||||||
|
@ -758,11 +769,11 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
var res = false
|
var res = false
|
||||||
if !cItem.isDeletedContent() {
|
if !cItem.isDeletedContent() {
|
||||||
res = chatModel.upsertChatItem(cInfo, cItem)
|
res = m.upsertChatItem(cInfo, cItem)
|
||||||
}
|
}
|
||||||
if res {
|
if res {
|
||||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
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 {
|
switch cItem.meta.itemStatus {
|
||||||
case .sndSent: endTask()
|
case .sndSent: endTask()
|
||||||
case .sndErrorAuth: endTask()
|
case .sndErrorAuth: endTask()
|
||||||
|
@ -773,22 +784,22 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||||
case let .chatItemUpdated(aChatItem):
|
case let .chatItemUpdated(aChatItem):
|
||||||
let cInfo = aChatItem.chatInfo
|
let cInfo = aChatItem.chatInfo
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
if m.upsertChatItem(cInfo, cItem) {
|
||||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||||
}
|
}
|
||||||
case let .chatItemDeleted(_, toChatItem):
|
case let .chatItemDeleted(_, toChatItem):
|
||||||
let cInfo = toChatItem.chatInfo
|
let cInfo = toChatItem.chatInfo
|
||||||
let cItem = toChatItem.chatItem
|
let cItem = toChatItem.chatItem
|
||||||
if cItem.meta.itemDeleted {
|
if cItem.meta.itemDeleted {
|
||||||
chatModel.removeChatItem(cInfo, cItem)
|
m.removeChatItem(cInfo, cItem)
|
||||||
} else {
|
} else {
|
||||||
// currently only broadcast deletion of rcv message can be received, and only this case should happen
|
// 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):
|
case let .rcvFileComplete(aChatItem):
|
||||||
let cInfo = aChatItem.chatInfo
|
let cInfo = aChatItem.chatInfo
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
if m.upsertChatItem(cInfo, cItem) {
|
||||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -21,6 +21,8 @@ struct ChatListNavLink: View {
|
||||||
groupNavLink(groupInfo)
|
groupNavLink(groupInfo)
|
||||||
case let .contactRequest(cReq):
|
case let .contactRequest(cReq):
|
||||||
contactRequestNavLink(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 {
|
private func deleteContactAlert(_ contact: Contact) -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Delete contact?"),
|
title: Text("Delete contact?"),
|
||||||
|
@ -137,7 +164,7 @@ struct ChatListNavLink: View {
|
||||||
chatModel.removeChat(contact.id)
|
chatModel.removeChat(contact.id)
|
||||||
}
|
}
|
||||||
} catch let error {
|
} 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 {
|
private func pendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Contact is not connected yet!"),
|
title: Text("Contact is not connected yet!"),
|
||||||
|
|
|
@ -13,6 +13,7 @@ struct ChatListView: View {
|
||||||
// not really used in this view
|
// not really used in this view
|
||||||
@State private var showSettings = false
|
@State private var showSettings = false
|
||||||
@State private var searchText = ""
|
@State private var searchText = ""
|
||||||
|
@AppStorage("pendingConnections") private var pendingConnections = true
|
||||||
|
|
||||||
var user: User
|
var user: User
|
||||||
|
|
||||||
|
@ -64,9 +65,16 @@ struct ChatListView: View {
|
||||||
|
|
||||||
private func filteredChats() -> [Chat] {
|
private func filteredChats() -> [Chat] {
|
||||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||||
return s == ""
|
return s == "" && pendingConnections
|
||||||
? chatModel.chats
|
? 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 {
|
private func connectViaUrlAlert(_ url: URL) -> Alert {
|
||||||
|
|
55
apps/ios/Shared/Views/ChatList/ContactConnectionView.swift
Normal file
55
apps/ios/Shared/Views/ChatList/ContactConnectionView.swift
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ struct ContactRequestView: View {
|
||||||
.padding(.leading, 4)
|
.padding(.leading, 4)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text(ChatInfo.contactRequest(contactRequest: contactRequest).chatViewName)
|
Text(contactRequest.chatViewName)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.foregroundColor(.blue)
|
.foregroundColor(.blue)
|
||||||
|
@ -28,7 +28,7 @@ struct ContactRequestView: View {
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
.frame(maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxHeight: .infinity, alignment: .topLeading)
|
||||||
Spacer()
|
Spacer()
|
||||||
timestampText(contactRequest.createdAt)
|
timestampText(contactRequest.updatedAt)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(.trailing, 8)
|
.padding(.trailing, 8)
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
|
|
@ -18,7 +18,8 @@ struct SettingsView: View {
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@Binding var showSettings: Bool
|
@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 showNotificationsAlert: Bool = false
|
||||||
@State var whichNotificationsAlert = NotificationAlert.enable
|
@State var whichNotificationsAlert = NotificationAlert.enable
|
||||||
|
|
||||||
|
@ -59,6 +60,11 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Settings") {
|
Section("Settings") {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "link")
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
Toggle("Show pending connections", isOn: $pendingConnections)
|
||||||
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
SMPServers()
|
SMPServers()
|
||||||
.navigationTitle("Your SMP servers")
|
.navigationTitle("Your SMP servers")
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
|
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
|
||||||
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
|
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
|
||||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.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 */; };
|
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
||||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
||||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.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 = "<group>"; };
|
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
|
||||||
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
|
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
|
||||||
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
|
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
|
||||||
|
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
|
||||||
|
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||||
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
|
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
|
||||||
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
||||||
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -286,6 +289,7 @@
|
||||||
5CA059C5279559F40002BEB4 /* Assets.xcassets */,
|
5CA059C5279559F40002BEB4 /* Assets.xcassets */,
|
||||||
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */,
|
5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */,
|
||||||
5C764E7F279C7276000C6508 /* dummy.m */,
|
5C764E7F279C7276000C6508 /* dummy.m */,
|
||||||
|
5C13730C2815740A00F43030 /* DebugJSON.playground */,
|
||||||
);
|
);
|
||||||
path = Shared;
|
path = Shared;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -342,6 +346,7 @@
|
||||||
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */,
|
5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */,
|
||||||
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */,
|
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */,
|
||||||
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */,
|
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */,
|
||||||
|
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
|
||||||
);
|
);
|
||||||
path = ChatList;
|
path = ChatList;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -483,6 +488,7 @@
|
||||||
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
|
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
|
||||||
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
|
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
|
||||||
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
|
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
|
||||||
|
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
|
||||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
||||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
|
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
|
||||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||||
|
|
|
@ -365,8 +365,11 @@ processChatCommand = \case
|
||||||
unsetActive $ ActiveC localDisplayName
|
unsetActive $ ActiveC localDisplayName
|
||||||
pure $ CRContactDeleted ct
|
pure $ CRContactDeleted ct
|
||||||
gs -> throwChatError $ CEContactGroups ct gs
|
gs -> throwChatError $ CEContactGroups ct gs
|
||||||
CTContactConnection ->
|
CTContactConnection -> withChatLock . procCmd $ do
|
||||||
CRContactConnectionDeleted <$> withStore (\st -> deletePendingContactConnection st userId chatId)
|
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"
|
CTGroup -> pure $ chatCmdError "not implemented"
|
||||||
CTContactRequest -> pure $ chatCmdError "not supported"
|
CTContactRequest -> pure $ chatCmdError "not supported"
|
||||||
APIAcceptContact connReqId -> withUser $ \user@User {userId} -> withChatLock $ do
|
APIAcceptContact connReqId -> withUser $ \user@User {userId} -> withChatLock $ do
|
||||||
|
|
|
@ -152,6 +152,7 @@ module Simplex.Chat.Store
|
||||||
updateGroupChatItemsRead,
|
updateGroupChatItemsRead,
|
||||||
getSMPServers,
|
getSMPServers,
|
||||||
overwriteSMPServers,
|
overwriteSMPServers,
|
||||||
|
getPendingContactConnection,
|
||||||
deletePendingContactConnection,
|
deletePendingContactConnection,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
@ -2727,21 +2728,39 @@ getContactConnectionChatPreviews_ db User {userId} _ =
|
||||||
stats = ChatStats {unreadCount = 0, minUnreadItemId = 0}
|
stats = ChatStats {unreadCount = 0, minUnreadItemId = 0}
|
||||||
in AChat SCTContactConnection $ Chat (ContactConnection conn) [] stats
|
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 =
|
deletePendingContactConnection st userId connId =
|
||||||
liftIOEither . withTransaction st $ \db -> runExceptT $ do
|
liftIO . withTransaction st $ \db ->
|
||||||
conn <-
|
DB.execute
|
||||||
ExceptT . firstRow toPendingContactConnection (SEPendingConnectionNotFound connId) $
|
db
|
||||||
DB.query
|
[sql|
|
||||||
db
|
DELETE FROM connections
|
||||||
[sql|
|
WHERE user_id = ?
|
||||||
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, created_at, updated_at
|
AND connection_id = ?
|
||||||
FROM connections
|
AND conn_type = ?
|
||||||
WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL
|
AND contact_id IS NULL
|
||||||
|]
|
AND conn_level = 0
|
||||||
(userId, ConnContact)
|
AND via_contact IS NULL
|
||||||
liftIO $ DB.execute db "DELETE FROM connections WHERE connection_id = ?" (Only connId)
|
|]
|
||||||
pure conn
|
(userId, connId, ConnContact)
|
||||||
|
|
||||||
toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, UTCTime, UTCTime) -> PendingContactConnection
|
toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, UTCTime, UTCTime) -> PendingContactConnection
|
||||||
toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, createdAt, updatedAt) =
|
toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, createdAt, updatedAt) =
|
||||||
|
|
|
@ -684,6 +684,9 @@ data PendingContactConnection = PendingContactConnection
|
||||||
}
|
}
|
||||||
deriving (Eq, Show, Generic)
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
aConnId' :: PendingContactConnection -> ConnId
|
||||||
|
aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId
|
||||||
|
|
||||||
instance ToJSON PendingContactConnection where toEncoding = J.genericToEncoding J.defaultOptions
|
instance ToJSON PendingContactConnection where toEncoding = J.genericToEncoding J.defaultOptions
|
||||||
|
|
||||||
data ConnStatus
|
data ConnStatus
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue