mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-29 01:09:15 +00:00
wip
This commit is contained in:
@@ -133,6 +133,7 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
|
||||
case apiChangeConnectionUser(connId: Int64, userId: Int64)
|
||||
case apiConnectPlan(userId: Int64, connLink: String, resolveMode: PlanResolveMode, linkOwnerSig: LinkOwnerSig?)
|
||||
case apiSendServiceRequest(userId: Int64, target: String, timeoutSec: Double?, requestJSON: String)
|
||||
case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain?)
|
||||
case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain?)
|
||||
case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64)
|
||||
@@ -355,6 +356,9 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
let resolveStr = resolveMode == .unknown ? "" : " resolve=\(resolveMode.rawValue)"
|
||||
let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" }
|
||||
return "/_connect plan \(userId) \(connLink)\(resolveStr)\(sigStr)"
|
||||
case let .apiSendServiceRequest(userId, target, timeoutSec, requestJSON):
|
||||
let timeoutStr = if let timeoutSec { " timeout=\(timeoutSec)" } else { "" }
|
||||
return "/_service_request \(userId) \(target)\(timeoutStr) \(requestJSON)"
|
||||
case let .apiPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain): return "/_prepare contact \(userId) \(connLink.cmdString)\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(contactShortLinkData))"
|
||||
case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain): return "/_prepare group \(userId) \(connLink.cmdString) direct=\(onOff(directLink))\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(groupShortLinkData))"
|
||||
case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)"
|
||||
@@ -543,6 +547,7 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
|
||||
case .apiChangeConnectionUser: return "apiChangeConnectionUser"
|
||||
case .apiConnectPlan: return "apiConnectPlan"
|
||||
case .apiSendServiceRequest: return "apiSendServiceRequest"
|
||||
case .apiPrepareContact: return "apiPrepareContact"
|
||||
case .apiPrepareGroup: return "apiPrepareGroup"
|
||||
case .apiChangePreparedContactUser: return "apiChangePreparedContactUser"
|
||||
@@ -821,6 +826,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
||||
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
||||
case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
|
||||
case connectionPlan(user: UserRef, connLink: CreatedConnLink, planSimplexName: SimplexNameInfo?, otherSimplexName: SimplexNameInfo?, connectionPlan: ConnectionPlan)
|
||||
case serviceResponse(user: UserRef, responseData: JSONValue)
|
||||
case newPreparedChat(user: UserRef, chat: ChatData)
|
||||
case contactUserChanged(user: UserRef, fromContact: Contact, newUser: UserRef, toContact: Contact)
|
||||
case groupUserChanged(user: UserRef, fromGroup: GroupInfo, newUser: UserRef, toGroup: GroupInfo)
|
||||
@@ -865,6 +871,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
||||
case .connectionIncognitoUpdated: "connectionIncognitoUpdated"
|
||||
case .connectionUserChanged: "connectionUserChanged"
|
||||
case .connectionPlan: "connectionPlan"
|
||||
case .serviceResponse: "serviceResponse"
|
||||
case .newPreparedChat: "newPreparedChat"
|
||||
case .contactUserChanged: "contactUserChanged"
|
||||
case .groupUserChanged: "groupUserChanged"
|
||||
|
||||
@@ -298,30 +298,47 @@ class ChatItemDummyModel: ObservableObject {
|
||||
func sendUpdate() { objectWillChange.send() }
|
||||
}
|
||||
|
||||
// A directory search and a connection can overlap - tapping a result starts a connection while
|
||||
// the search is still running - so the single progress slot records its owner.
|
||||
enum ConnectProgressOwner {
|
||||
case connect
|
||||
case directorySearch
|
||||
}
|
||||
|
||||
class ConnectProgressManager: ObservableObject {
|
||||
@Published private var connectInProgress: String? = nil
|
||||
@Published private var connectProgressByTimeout: Bool = false
|
||||
private var onCancel: (() -> Void)?
|
||||
private var owner: ConnectProgressOwner?
|
||||
|
||||
static let shared = ConnectProgressManager()
|
||||
|
||||
func startConnectProgress(_ text: String, onCancel: (() -> Void)? = nil) {
|
||||
func startConnectProgress(_ text: String, owner: ConnectProgressOwner = .connect, onCancel: (() -> Void)? = nil) {
|
||||
connectInProgress = text
|
||||
self.owner = owner
|
||||
self.onCancel = onCancel
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.connectProgressByTimeout = self.connectInProgress != nil
|
||||
}
|
||||
}
|
||||
|
||||
func stopConnectProgress() {
|
||||
// a late directory search result must not clear the spinner that now belongs to a connection
|
||||
func stopConnectProgress(_ owner: ConnectProgressOwner = .connect) {
|
||||
if let current = self.owner, current != owner { return }
|
||||
connectInProgress = nil
|
||||
self.owner = nil
|
||||
onCancel = nil
|
||||
connectProgressByTimeout = false
|
||||
}
|
||||
|
||||
// a user-initiated cancel, and the takeover in planAndConnect, cancel whatever is running
|
||||
func cancelConnectProgress() {
|
||||
onCancel?()
|
||||
stopConnectProgress()
|
||||
let cancel = onCancel
|
||||
owner = nil
|
||||
onCancel = nil
|
||||
connectInProgress = nil
|
||||
connectProgressByTimeout = false
|
||||
cancel?()
|
||||
}
|
||||
|
||||
var showConnectProgress: String? {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// DirectorySearch.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by spaced4ndy on 12.08.2026.
|
||||
// Copyright © 2026 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
|
||||
// The directory's contact address, as published in docs/DIRECTORY.md. It must be the short
|
||||
// link: only that form carries the address DR keys that service requests require, so the full
|
||||
// links on the What's New cards cannot be substituted here.
|
||||
let DIRECTORY_SERVICE_LINK = "https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok"
|
||||
|
||||
// A service request is a full DR handshake, so it is slower than a local API call; the user
|
||||
// gets a cancellable spinner while it runs and a retry row if it times out.
|
||||
let DIRECTORY_SEARCH_TIMEOUT_SEC: Double = 10
|
||||
|
||||
struct DirectoryPublicLink: Decodable, Hashable {
|
||||
var connFullLink: String? = nil
|
||||
var connShortLink: String? = nil
|
||||
}
|
||||
|
||||
struct DirectoryEntryType: Decodable, Hashable {
|
||||
var groupType: GroupType? = nil
|
||||
var summary: GroupSummary
|
||||
}
|
||||
|
||||
struct DirectorySearchEntry: Decodable, Hashable, Identifiable {
|
||||
var entryType: DirectoryEntryType
|
||||
var displayName: String
|
||||
var simplexName: String? = nil
|
||||
var groupLink: DirectoryPublicLink
|
||||
var shortDescr: String? = nil
|
||||
var image: String? = nil
|
||||
var activeAt: Date? = nil
|
||||
var createdAt: Date? = nil
|
||||
|
||||
// the directory drops entries with no link, but the response is untrusted input
|
||||
var connectLink: String? { groupLink.connShortLink ?? groupLink.connFullLink }
|
||||
|
||||
// the link is stable and unique per entry, so it also de-duplicates across pages
|
||||
var id: String { connectLink ?? displayName }
|
||||
}
|
||||
|
||||
private struct DirectorySearchResponse: Decodable {
|
||||
var type: String
|
||||
var entries: [DirectorySearchEntry]?
|
||||
var searchCursor: JSONValue?
|
||||
}
|
||||
|
||||
struct DirectorySearchResults {
|
||||
var entries: [DirectorySearchEntry]
|
||||
// opaque: stored and echoed back on the next request, never inspected
|
||||
var cursor: JSONValue?
|
||||
}
|
||||
|
||||
func directorySearchRequestJSON(_ text: String, _ cursor: JSONValue?) -> String {
|
||||
var req: [String: JSONValue] = ["type": .string("search"), "searchText": .string(text)]
|
||||
if let cursor { req["searchCursor"] = cursor }
|
||||
return encodeJSON(JSONValue.object(req))
|
||||
}
|
||||
|
||||
// The response is a tagged object: searchResults or error. Anything else is a failure rather
|
||||
// than something to parse leniently - it comes from outside the app.
|
||||
func parseDirectorySearchResponse(_ resp: JSONValue) -> DirectorySearchResults? {
|
||||
guard let r: DirectorySearchResponse = decodeJSONValue(resp), r.type == "searchResults" else {
|
||||
return nil
|
||||
}
|
||||
return DirectorySearchResults(entries: r.entries ?? [], cursor: r.searchCursor)
|
||||
}
|
||||
@@ -1032,6 +1032,23 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi
|
||||
if let r { throw r.unexpected } else { return nil }
|
||||
}
|
||||
|
||||
// Blocks until the directory replies or the timeout elapses.
|
||||
func apiSearchDirectory(_ text: String, cursor: JSONValue?) async -> DirectorySearchResults? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiSearchDirectory: no current user")
|
||||
return nil
|
||||
}
|
||||
let req = directorySearchRequestJSON(text, cursor)
|
||||
let r: APIResult<ChatResponse1> = await chatApiSendCmd(
|
||||
.apiSendServiceRequest(userId: userId, target: DIRECTORY_SERVICE_LINK, timeoutSec: DIRECTORY_SEARCH_TIMEOUT_SEC, requestJSON: req)
|
||||
)
|
||||
if case let .result(.serviceResponse(_, responseData)) = r {
|
||||
return parseDirectorySearchResponse(responseData)
|
||||
}
|
||||
logger.error("apiSearchDirectory error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiConnectPlan(connLink: String, resolveMode: PlanResolveMode = .unknown, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue<Bool>) async -> ConnectionPlanResult? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiConnectPlan: no current user")
|
||||
|
||||
Reference in New Issue
Block a user