ios: reliably show API alerts (use UIKit) (#7195)

* ios: reliably show API alerts (use UIKit)

* fix infinite recursion
This commit is contained in:
Evgeny
2026-07-03 13:47:45 +01:00
committed by GitHub
parent 10a814694c
commit e3502c5738
16 changed files with 287 additions and 301 deletions
+138 -148
View File
@@ -569,7 +569,7 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
return cItems
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
await MainActor.run { showAlert(networkErrorAlert) }
} else {
sendMessageErrorAlert(r.unexpected)
}
@@ -1003,15 +1003,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo
return nil
}
func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) {
func apiAddContact(incognito: Bool) async -> (CreatedConnLink, PendingContactConnection)? {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user")
return (nil, nil)
return nil
}
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) }
let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil }
return (nil, alert)
if case let .result(.invitation(_, connLinkInv, connection)) = r { return (connLinkInv, connection) }
if let r { await MainActor.run { showAlert(connectionErrorAlert(r)) } }
return nil
}
func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? {
@@ -1026,130 +1026,124 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi
if let r { throw r.unexpected } else { return nil }
}
func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue<Bool>) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) {
func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue<Bool>) async -> (CreatedConnLink, ConnectionPlan)? {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnectPlan: no current user")
return (nil, nil)
return nil
}
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress)
if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) }
let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil }
return (nil, alert)
if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return (connLink, connPlan) }
if let r { await apiConnectResponseAlert(r) }
return nil
}
func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? {
let (r, alert) = await apiConnect_(incognito: incognito, connLink: connLink)
if let alert = alert {
AlertManager.shared.showAlert(alert)
return nil
} else {
return r
}
}
func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqType, PendingContactConnection)?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user")
return (nil, nil)
return nil
}
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnect(userId: userId, incognito: incognito, connLink: connLink))
let m = ChatModel.shared
switch r {
case let .result(.sentConfirmation(_, connection)):
return ((.invitation, connection), nil)
return (.invitation, connection)
case let .result(.sentInvitation(_, connection)):
return ((.contact, connection), nil)
return (.contact, connection)
case let .result(.contactAlreadyExists(_, contact)):
if let c = m.getContactChat(contact.contactId) {
ItemsModel.shared.loadOpenChat(c.id)
}
let alert = contactAlreadyExistsAlert(contact)
return (nil, alert)
await contactAlreadyExistsAlert(contact)
return nil
default: ()
}
let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil }
return (nil, alert)
if let r { await apiConnectResponseAlert(r) }
return nil
}
private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> Alert {
switch r.unexpected {
case .error(.invalidConnReq):
mkAlert(
title: "Invalid connection link",
message: "Please check that you used the correct link or ask your contact to send you another one."
)
case .error(.unsupportedConnReq):
mkAlert(
title: "Unsupported connection link",
message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link."
)
case let .error(.simplexDomainNotReady(domain, err)):
switch err {
case .noValidLink:
mkAlert(
title: "No valid link",
message: "The SimpleX name \(domain.fullDomainName) is registered, but it has no valid link."
private func apiConnectResponseAlert<R>(_ r: APIResult<R>) async {
await MainActor.run {
switch r.unexpected {
case .error(.invalidConnReq):
showAlert(
NSLocalizedString("Invalid connection link", comment: ""),
message: NSLocalizedString("Please check that you used the correct link or ask your contact to send you another one.", comment: "")
)
case .unknownDomain:
mkAlert(
title: "Unconfirmed name",
message: "The SimpleX name \(domain.fullDomainName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner."
case .error(.unsupportedConnReq):
showAlert(
NSLocalizedString("Unsupported connection link", comment: ""),
message: NSLocalizedString("This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.", comment: "")
)
}
case .errorAgent(.NO_NAME_SERVERS):
mkAlert(
title: "SimpleX name error",
message: "None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link."
)
case .errorAgent(.SMP(_, .AUTH)):
mkAlert(
title: "Connection error (AUTH)",
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
)
case let .errorAgent(.SMP(_, .BLOCKED(info))):
Alert(
title: Text("Connection blocked"),
message: Text("Connection is blocked by server operator:\n\(info.reason.text)"),
primaryButton: .default(Text("Ok")),
secondaryButton: .default(Text("How it works")) {
DispatchQueue.main.async {
UIApplication.shared.open(contentModerationPostLink)
}
case let .error(.simplexDomainNotReady(domain, err)):
switch err {
case .noValidLink:
showAlert(
NSLocalizedString("No valid link", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but it has no valid link.", comment: ""), domain.fullDomainName)
)
case .unknownDomain:
showAlert(
NSLocalizedString("Unconfirmed name", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.", comment: ""), domain.fullDomainName)
)
}
)
case .errorAgent(.SMP(_, .QUOTA)):
mkAlert(
title: "Undelivered messages",
message: "The connection reached the limit of undelivered messages, your contact may be offline."
)
case let .errorAgent(.INTERNAL(internalErr)):
if internalErr == "SEUniqueID" {
mkAlert(
title: "Already connected?",
message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(internalErr))."
case .errorAgent(.NO_NAME_SERVERS):
showAlert(
NSLocalizedString("SimpleX name error", comment: ""),
message: NSLocalizedString("None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.", comment: "")
)
} else {
connectionErrorAlert(r)
case .errorAgent(.SMP(_, .AUTH)):
showAlert(
NSLocalizedString("Connection error (AUTH)", comment: ""),
message: NSLocalizedString("Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.", comment: "")
)
case let .errorAgent(.SMP(_, .BLOCKED(info))):
showAlert(
NSLocalizedString("Connection blocked", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Connection is blocked by server operator:\n%@", comment: ""), info.reason.text),
actions: {[
okAlertAction,
UIAlertAction(title: NSLocalizedString("How it works", comment: ""), style: .default) { _ in
DispatchQueue.main.async {
UIApplication.shared.open(contentModerationPostLink)
}
}
]}
)
case .errorAgent(.SMP(_, .QUOTA)):
showAlert(
NSLocalizedString("Undelivered messages", comment: ""),
message: NSLocalizedString("The connection reached the limit of undelivered messages, your contact may be offline.", comment: "")
)
case let .errorAgent(.INTERNAL(internalErr)):
if internalErr == "SEUniqueID" {
showAlert(
NSLocalizedString("Already connected?", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("It seems like you are already connected via this link. If it is not the case, there was an error (%@).", comment: ""), internalErr)
)
} else {
showAlert(connectionErrorAlert(r))
}
case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))):
switch nameErr {
case .NOT_FOUND:
showAlert(
NSLocalizedString("Name not found", comment: ""),
message: NSLocalizedString("This SimpleX name is not registered. Please check the name.", comment: "")
)
case .NO_RESOLVER:
showAlert(
NSLocalizedString("SimpleX name error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Server %@ does not support name resolution. Configure servers, or use a connection link.", comment: ""), serverAddress)
)
case let .RESOLVER(resolverErr):
showAlert(
NSLocalizedString("SimpleX name error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Resolver error: %@", comment: ""), resolverErr)
)
}
default: showAlert(connectionErrorAlert(r))
}
case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))):
switch nameErr {
case .NOT_FOUND:
mkAlert(
title: "Name not found",
message: "This SimpleX name is not registered. Please check the name."
)
case .NO_RESOLVER:
mkAlert(
title: "SimpleX name error",
message: "Server \(serverAddress) does not support name resolution. Configure servers, or use a connection link."
)
case let .RESOLVER(resolverErr):
mkAlert(
title: "SimpleX name error",
message: "Resolver error: \(resolverErr)"
)
}
default: connectionErrorAlert(r)
}
}
@@ -1162,36 +1156,34 @@ func connErrorText(_ e: ChatError) -> String {
case .errorAgent(.SMP(_, .AUTH)):
NSLocalizedString("Connection error (AUTH)", comment: "conn error description")
case let .errorAgent(.SMP(_, .BLOCKED(info))):
NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description")
String.localizedStringWithFormat(NSLocalizedString("Connection blocked: %@", comment: "conn error description"), info.reason.text)
case .errorAgent(.SMP(_, .QUOTA)):
NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description")
default:
if getNetworkErrorAlert(e) != nil {
NSLocalizedString("Network error", comment: "conn error description")
} else {
"\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))"
String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "conn error description"), responseError(e))
}
}
}
func contactAlreadyExistsAlert(_ contact: Contact) -> Alert {
mkAlert(
title: "Contact already exists",
message: "You are already connected to \(contact.displayName)."
)
}
private func connectionErrorAlert<R>(_ r: APIResult<R>) -> Alert {
if let networkErrorAlert = networkErrorAlert(r) {
return networkErrorAlert
} else {
return mkAlert(
title: "Connection error",
message: "Error: \(responseError(r.unexpected))"
func contactAlreadyExistsAlert(_ contact: Contact) async {
await MainActor.run {
showAlert(
NSLocalizedString("Contact already exists", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("You are already connected to %@.", comment: ""), contact.displayName)
)
}
}
private func connectionErrorAlert<R>(_ r: APIResult<R>) -> (title: String, message: String?) {
networkErrorAlert(r) ?? (
title: NSLocalizedString("Connection error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected))
)
}
func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData {
let userId = try currentUserId("apiPrepareContact")
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain))
@@ -1221,30 +1213,29 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws -
func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? {
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg))
if case let .result(.startedConnectionToContact(_, contact)) = r { return contact }
if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) }
if let r { await apiConnectResponseAlert(r) }
return nil
}
func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> (GroupInfo, [RelayConnectionResult])? {
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg))
if case let .result(.startedConnectionToGroup(_, groupInfo, relayResults)) = r { return (groupInfo, relayResults) }
if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) }
if let r { await apiConnectResponseAlert(r) }
return nil
}
func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) {
func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> Contact? {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnectContactViaAddress: no current user")
return (nil, nil)
return nil
}
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId))
if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) }
if case let .result(.sentInvitationToContact(_, contact, _)) = r { return contact }
if let r {
logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))")
return (nil, connectionErrorAlert(r))
} else {
return (nil, nil)
await MainActor.run { showAlert(connectionErrorAlert(r)) }
}
return nil
}
func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws {
@@ -1365,16 +1356,16 @@ func apiSetProfileAddress(on: Bool) async throws -> User? {
}
}
// name is the encoded SimplexName (e.g. "@alice.simplex"); nil clears it
// owner-specific SNENoValidLink wording; everything else reuses the general apiConnectResponseAlert
func showSetSimplexNameError<R>(_ r: APIResult<R>, isChannel: Bool) {
func showSetSimplexNameError<R>(_ r: APIResult<R>, isChannel: Bool) async {
if case let .error(.simplexDomainNotReady(domain, .noValidLink)) = r.unexpected {
let format = isChannel
? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message")
: NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message")
showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName))
await MainActor.run {
showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName))
}
} else {
AlertManager.shared.showAlert(apiConnectResponseAlert(r))
await apiConnectResponseAlert(r)
}
}
@@ -1385,7 +1376,7 @@ func apiSetUserDomain(_ simplexDomain: String?) async throws -> User {
case let .result(.userProfileUpdated(user, _, _, _)): return user
case let .result(.userProfileNoChange(user)): return user
default:
showSetSimplexNameError(r, isChannel: false)
await showSetSimplexNameError(r, isChannel: false)
throw r.unexpected
}
}
@@ -1502,23 +1493,22 @@ func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> User
func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? {
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId))
let am = AlertManager.shared
if case let .result(.acceptingContactRequest(_, contact)) = r { return contact }
if case .error(.errorAgent(.SMP(_, .AUTH))) = r {
am.showAlertMsg(
title: "Connection error (AUTH)",
message: "Sender may have deleted the connection request."
)
await MainActor.run { showAlert(
NSLocalizedString("Connection error (AUTH)", comment: ""),
message: NSLocalizedString("Sender may have deleted the connection request.", comment: "")
) }
} else if let r {
if let networkErrorAlert = networkErrorAlert(r) {
am.showAlert(networkErrorAlert)
await MainActor.run { showAlert(networkErrorAlert) }
} else {
logger.error("apiAcceptContactRequest error: \(String(describing: r))")
am.showAlertMsg(
title: "Error accepting contact request",
message: "Error: \(responseError(r.unexpected))"
)
await MainActor.run { showAlert(
NSLocalizedString("Error accepting contact request", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected))
) }
}
}
return nil
@@ -1762,11 +1752,11 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws {
try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId))
}
func networkErrorAlert<R>(_ res: APIResult<R>) -> Alert? {
if case let .error(e) = res, let alert = getNetworkErrorAlert(e) {
return mkAlert(title: alert.title, message: alert.message)
func networkErrorAlert<R>(_ res: APIResult<R>) -> (title: String, message: String?)? {
if case let .error(e) = res {
getNetworkErrorAlert(e)
} else {
return nil
nil
}
}
@@ -2071,7 +2061,7 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
func apiSetPublicGroupAccess(_ groupId: Int64, access: PublicGroupAccess) async throws -> GroupInfo {
let r: APIResult<ChatResponse2> = await chatApiSendCmd(.apiSetPublicGroupAccess(groupId: groupId, access: access))
if case let .result(.groupUpdated(_, toGroup)) = r { return toGroup }
showSetSimplexNameError(r, isChannel: true)
await showSetSimplexNameError(r, isChannel: true)
throw r.unexpected
}
@@ -2129,7 +2119,7 @@ func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async
func apiAcceptMemberContact(contactId: Int64) async -> Contact? {
let r: APIResult<ChatResponse2>? = await chatApiSendCmdWithRetry(.apiAcceptMemberContact(contactId: contactId))
if case let .result(.memberContactAccepted(_, contact)) = r { return contact }
if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) }
if let r { await apiConnectResponseAlert(r) }
return nil
}
+10 -17
View File
@@ -283,8 +283,9 @@ struct ChatInfoView: View {
}
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
await MainActor.run {
showErrorAlert(e, NSLocalizedString("Error", comment: ""))
}
}
}
}
@@ -334,7 +335,7 @@ struct ChatInfoView: View {
case .syncConnectionForceAlert:
return syncConnectionForceAlert({
Task {
if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) {
if let stats = await syncContactConnection(contact, force: true) {
connectionStats = stats
dismiss()
}
@@ -541,7 +542,7 @@ struct ChatInfoView: View {
private func synchronizeConnectionButton() -> some View {
Button {
Task {
if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) {
if let stats = await syncContactConnection(contact, force: false) {
connectionStats = stats
dismiss()
}
@@ -618,9 +619,8 @@ struct ChatInfoView: View {
}
} catch let error {
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error changing address", comment: ""))
}
}
}
@@ -636,9 +636,8 @@ struct ChatInfoView: View {
}
} catch let error {
logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: ""))
}
}
}
@@ -748,7 +747,7 @@ struct ChatTTLOption: View {
}
}
func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? {
func syncContactConnection(_ contact: Contact, force: Bool) async -> ConnectionStats? {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
await MainActor.run {
@@ -757,14 +756,8 @@ func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAler
return stats
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
showAlert(
SomeAlert(
alert: mkAlert(title: a.title, message: a.message),
id: "syncContactConnection error"
)
)
showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: ""))
}
return nil
}
@@ -844,7 +837,7 @@ private struct CallButton: View {
message: Text("Connection requires encryption renegotiation."),
primaryButton: .default(Text("Fix")) {
Task {
if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) {
if let stats = await syncContactConnection(contact, force: false) {
connectionStats = stats
}
}
@@ -185,9 +185,8 @@ struct CIRcvDecryptionError: View {
}
} catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: ""))
}
}
}
@@ -202,9 +201,8 @@ struct CIRcvDecryptionError: View {
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: ""))
}
}
}
@@ -174,8 +174,9 @@ struct AddGroupMembersViewCommon: View {
}
addedMembersCb(selectedContacts)
} catch {
let a = getErrorAlert(error, "Error adding member(s)")
alert = .error(title: a.title, error: a.message)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error adding member(s)", comment: ""))
}
}
}
}
@@ -155,8 +155,9 @@ struct GroupLinkView: View {
do {
groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
} catch let error {
let a = getErrorAlert(error, "Error updating group link")
alert = .error(title: a.title, error: a.message)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error updating group link", comment: ""))
}
}
}
}
@@ -188,8 +189,7 @@ struct GroupLinkView: View {
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
await MainActor.run {
creatingLink = false
let a = getErrorAlert(error, "Error creating group link")
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error creating group link", comment: ""))
}
}
}
@@ -230,8 +230,7 @@ struct GroupLinkView: View {
logger.error("apiAddGroupShortLink: \(responseError(error))")
await MainActor.run {
creatingLink = false
let a = getErrorAlert(error, "Error adding short link")
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error adding short link", comment: ""))
}
}
}
@@ -278,8 +278,9 @@ struct GroupMemberInfoView: View {
}
} catch let e {
logger.error("apiContactQueueInfo error: \(responseError(e))")
let a = getErrorAlert(e, "Error")
await MainActor.run { alert = .error(title: a.title, error: a.message) }
await MainActor.run {
showErrorAlert(e, NSLocalizedString("Error", comment: ""))
}
}
}
}
@@ -473,10 +474,9 @@ struct GroupMemberInfoView: View {
}
} catch let error {
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error creating member contact")
await MainActor.run {
progressIndicator = false
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error creating member contact", comment: ""))
}
}
}
@@ -752,8 +752,9 @@ struct GroupMemberInfoView: View {
} catch let error {
newRole = mem.memberRole
logger.error("apiMembersRole error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing role")
alert = .error(title: a.title, error: a.message)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error changing role", comment: ""))
}
}
}
},
@@ -774,9 +775,8 @@ struct GroupMemberInfoView: View {
}
} catch let error {
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error changing address")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error changing address", comment: ""))
}
}
}
@@ -792,9 +792,8 @@ struct GroupMemberInfoView: View {
}
} catch let error {
logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))")
let a = getErrorAlert(error, "Error aborting address change")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: ""))
}
}
}
@@ -811,9 +810,8 @@ struct GroupMemberInfoView: View {
}
} catch let error {
logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: ""))
}
}
}
@@ -532,9 +532,7 @@ struct ChatListNavLink: View {
.frameCompat(height: dynamicRowHeight)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection) { a in
AlertManager.shared.showAlertMsg(title: a.title, message: a.message)
})
AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection))
} label: {
deleteLabel
}
@@ -698,7 +696,7 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert {
)
}
func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert {
func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, success: @escaping () -> Void = {}) -> Alert {
Alert(
title: Text("Delete pending connection?"),
message: Text(contactConnection.displayName + "\n\n")
@@ -715,7 +713,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection,
}
} catch let error {
await MainActor.run {
showError(getErrorAlert(error, "Error deleting connection"))
showErrorAlert(error, NSLocalizedString("Error deleting connection", comment: ""))
}
}
}
@@ -725,11 +723,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection,
}
func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: (Alert) -> Void) async -> Bool {
let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId)
if let alert = alert {
showAlert(alert)
return false
} else if let contact = contact {
if let contact = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) {
await MainActor.run {
ChatModel.shared.updateContact(contact)
}
@@ -757,8 +751,9 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
await onComplete()
} catch let error {
await onComplete()
let a = getErrorAlert(error, "Error joining group")
AlertManager.shared.showAlertMsg(title: a.title, message: a.message)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error joining group", comment: ""))
}
}
func deleteGroup() async {
@@ -773,12 +768,13 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
}
}
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
func showErrorAlert(_ error: Error, _ title: String) {
let err = { String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(error)) }
if let r = error as? ChatError,
let alert = getNetworkErrorAlert(r) {
return alert
showAlert(alert.title, message: alert.message ?? err())
} else {
return ErrorAlert(title: title, message: "Error: \(responseError(error))")
showAlert(title, message: err())
}
}
@@ -103,11 +103,7 @@ struct ContactConnectionInfo: View {
.alert(item: $alert) { _alert in
switch _alert {
case .deleteInvitationAlert:
return deleteContactConnectionAlert(contactConnection) { a in
alert = .error(title: a.title, error: a.message)
} success: {
dismiss()
}
return deleteContactConnectionAlert(contactConnection, success: { dismiss() })
case let .error(title, error): return mkAlert(title: title, message: error)
}
}
@@ -54,6 +54,10 @@ func showAlert(
}
}
func showAlert(_ a: (title: String, message: String?)) {
showAlert(a.title, message: a.message)
}
func showAlert(
_ title: String,
message: String? = nil,
@@ -204,8 +204,7 @@ struct NewChatView: View {
creatingConnReq = true
Task {
_ = try? await Task.sleep(nanoseconds: 250_000000)
let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get())
if let (connLink, pcc) = r {
if let (connLink, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
await MainActor.run {
m.updateContactConnection(pcc)
m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false)
@@ -215,9 +214,6 @@ struct NewChatView: View {
} else {
await MainActor.run {
creatingConnReq = false
if let apiAlert = apiAlert {
alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error"))
}
}
}
}
@@ -434,15 +430,9 @@ private struct ActiveProfilePicker: View {
profileSwitchStatus = .idle
incognitoEnabled = !incognito
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
let err = getErrorAlert(error, "Error changing to incognito!")
alert = SomeAlert(
alert: Alert(
title: Text(err.title),
message: Text(err.message ?? "Error: \(responseError(error))")
),
id: "setConnectionIncognitoError"
)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error changing to incognito!", comment: ""))
}
}
}
}
@@ -494,14 +484,7 @@ private struct ActiveProfilePicker: View {
if let currentUser = chatModel.currentUser {
selectedProfile = currentUser
}
let err = getErrorAlert(error, "Error changing connection profile")
alert = SomeAlert(
alert: Alert(
title: Text(err.title),
message: Text(err.message ?? "Error: \(responseError(error))")
),
id: "changeConnectionUserError"
)
showErrorAlert(error, NSLocalizedString("Error changing connection profile", comment: ""))
}
}
}
@@ -1331,7 +1314,7 @@ func planAndConnect(
func connectTask(_ inProgress: BoxedValue<Bool>) {
Task {
let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress)
let result = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress)
await MainActor.run {
ConnectProgressManager.shared.stopConnectProgress()
}
@@ -1623,17 +1606,8 @@ func planAndConnect(
cleanup: cleanup
)
}
} else {
await MainActor.run {
if let alert {
dismissAllSheets(animated: true) {
AlertManager.shared.showAlert(alert)
cleanup?()
}
} else {
cleanup?()
}
}
} else if let cleanup {
await MainActor.run { cleanup() }
}
}
}
@@ -86,12 +86,10 @@ struct CreateSimpleXAddress: View {
await MainActor.run { progressIndicator = false }
} catch let error {
logger.error("CreateSimpleXAddress create address: \(responseError(error))")
await MainActor.run { progressIndicator = false }
let a = getErrorAlert(error, "Error creating address")
AlertManager.shared.showAlertMsg(
title: a.title,
message: a.message
)
await MainActor.run {
progressIndicator = false
showErrorAlert(error, NSLocalizedString("Error creating address", comment: ""))
}
}
}
} label: {
@@ -156,11 +154,7 @@ struct CreateSimpleXAddress: View {
}
case let .failure(error):
logger.error("CreateSimpleXAddress share via email: \(responseError(error))")
let a = getErrorAlert(error, "Error sending email")
AlertManager.shared.showAlertMsg(
title: a.title,
message: a.message
)
showErrorAlert(error, NSLocalizedString("Error sending email", comment: ""))
}
mailViewResult = nil
}
@@ -180,11 +180,9 @@ struct YourNetworkView: View {
m.notificationMode = notificationMode
}
} catch let error {
let a = getErrorAlert(error, "Error enabling notifications")
AlertManager.shared.showAlertMsg(
title: a.title,
message: a.message
)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error enabling notifications", comment: ""))
}
}
}
}
@@ -535,8 +535,7 @@ struct ConnectDesktopView: View {
}
private func errorAlert(_ error: Error) {
let a = getErrorAlert(error, "Error")
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error", comment: ""))
}
}
@@ -316,9 +316,10 @@ struct UserAddressView: View {
}
} catch let error {
logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))")
let a = getErrorAlert(error, "Error creating address")
alert = .error(title: a.title, error: a.message)
await MainActor.run { progressIndicator = false }
await MainActor.run {
progressIndicator = false
showErrorAlert(error, NSLocalizedString("Error creating address", comment: ""))
}
}
}
}
@@ -390,8 +391,7 @@ struct UserAddressView: View {
case .success: ()
case let .failure(error):
logger.error("UserAddressView share via email: \(responseError(error))")
let a = getErrorAlert(error, "Error sending email")
alert = .error(title: a.title, error: a.message)
showErrorAlert(error, NSLocalizedString("Error sending email", comment: ""))
}
mailViewResult = nil
}
@@ -319,8 +319,9 @@ struct UserProfilesView: View {
}
} catch let error {
logger.error("Error deleting user profile: \(error)")
let a = getErrorAlert(error, "Error deleting user profile")
alert = .error(title: a.title, error: a.message)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error deleting user profile", comment: ""))
}
}
func deleteUser() async throws {
@@ -436,8 +437,9 @@ struct UserProfilesView: View {
}
}
} catch let error {
let a = getErrorAlert(error, "Error updating user privacy")
alert = .error(title: a.title, error: a.message)
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error updating user privacy", comment: ""))
}
}
}
}
+75 -31
View File
@@ -34,20 +34,16 @@ public struct ErrorAlert: Error {
}
public init(_ error: any Error) {
self = if let e = error as? ChatError {
ErrorAlert(e)
self = if let chatError = error as? ChatError {
if let a = getNetworkErrorAlert(chatError) {
ErrorAlert(title: "\(a.title)", message: a.message.map { "\($0)" })
} else {
ErrorAlert("\(chatErrorString(chatError))")
}
} else {
ErrorAlert("\(error.localizedDescription)")
}
}
public init(_ chatError: ChatError) {
self = if let networkErrorAlert = getNetworkErrorAlert(chatError) {
networkErrorAlert
} else {
ErrorAlert("\(chatErrorString(chatError))")
}
}
}
extension LocalizedStringKey: @unchecked Sendable { }
@@ -83,18 +79,33 @@ extension View {
}
}
public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? {
public func getNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String?)? {
switch e {
case let .errorAgent(.BROKER(addr, .TIMEOUT)):
ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
(
title: NSLocalizedString("Connection timeout", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr))
)
case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))):
ErrorAlert(title: "Connection error", message: "Fingerprint in server address does not match certificate: \(serverHostname(addr)).")
(
title: NSLocalizedString("Connection error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in server address does not match certificate: %@.", comment: ""), serverHostname(addr))
)
case let .errorAgent(.BROKER(addr, .NETWORK)):
ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
(
title: NSLocalizedString("Connection error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr))
)
case let .errorAgent(.BROKER(addr, .HOST)):
ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).")
(
title: NSLocalizedString("Connection error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Server address is incompatible with network settings: %@.", comment: ""), serverHostname(addr))
)
case let .errorAgent(.BROKER(addr, .TRANSPORT(.version))):
ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).")
(
title: NSLocalizedString("Connection error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Server version is incompatible with your app: %@.", comment: ""), serverHostname(addr))
)
case let .errorAgent(.SMP(serverAddress, .PROXY(proxyErr))):
smpProxyErrorAlert(proxyErr, serverAddress)
case let .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr)))):
@@ -103,39 +114,72 @@ public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? {
}
}
private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? {
private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> (title: String, message: String?)? {
switch proxyErr {
case .BROKER(brokerErr: .TIMEOUT):
return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr))
)
case .BROKER(brokerErr: .NETWORK(.unknownCAError)):
return ErrorAlert(title: "Private routing error", message: "Fingerprint in forwarding server address does not match certificate: \(serverHostname(srvAddr)).")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in forwarding server address does not match certificate: %@.", comment: ""), serverHostname(srvAddr))
)
case .BROKER(brokerErr: .NETWORK):
return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr))
)
case .BROKER(brokerErr: .HOST):
return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server address is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr))
)
case .BROKER(brokerErr: .TRANSPORT(.version)):
return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server version is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr))
)
default:
return nil
nil
}
}
private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? {
private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> (title: String, message: String?)? {
switch proxyErr {
case .BROKER(brokerErr: .TIMEOUT):
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer))
)
case .BROKER(brokerErr: .NETWORK(.unknownCAError)):
return ErrorAlert(title: "Private routing error", message: "Fingerprint in destination server address does not match certificate: \(serverHostname(relayServer)).")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in destination server address does not match certificate: %@.", comment: ""), serverHostname(relayServer))
)
case .BROKER(brokerErr: .NETWORK):
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer))
)
case .NO_SESSION:
return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer))
)
case .BROKER(brokerErr: .HOST):
return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Destination server address of %@ is incompatible with forwarding server %@ settings.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer))
)
case .BROKER(brokerErr: .TRANSPORT(.version)):
return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).")
(
title: NSLocalizedString("Private routing error", comment: ""),
message: String.localizedStringWithFormat(NSLocalizedString("Destination server version of %@ is incompatible with forwarding server %@.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer))
)
default:
return nil
nil
}
}