mirror of
https://github.com/signalapp/Signal-iOS.git
synced 2025-12-05 01:10:41 +00:00
Asyncify UsernameQuerier, use ActionSheetDisplayableError
This commit is contained in:
@@ -646,17 +646,17 @@ extension ConversationViewController: CVComponentDelegate {
|
||||
}
|
||||
|
||||
public func didTapUsernameLink(usernameLink: Usernames.UsernameLink) {
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
UsernameQuerier().queryForUsernameLink(
|
||||
Task {
|
||||
guard let (_, aci) = await UsernameQuerier().queryForUsernameLink(
|
||||
link: usernameLink,
|
||||
fromViewController: self,
|
||||
tx: tx,
|
||||
onSuccess: { _, aci in
|
||||
SignalApp.shared.presentConversationForAddress(
|
||||
SignalServiceAddress(aci),
|
||||
animated: true
|
||||
)
|
||||
}
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
SignalApp.shared.presentConversationForAddress(
|
||||
SignalServiceAddress(aci),
|
||||
animated: true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,17 +152,18 @@ class UrlOpener {
|
||||
SignalDotMePhoneNumberLink.openChat(url: url, fromViewController: rootViewController)
|
||||
|
||||
case .usernameLink(let link):
|
||||
databaseStorage.read { tx in
|
||||
UsernameQuerier().queryForUsernameLink(
|
||||
Task {
|
||||
guard let (_, aci) = await UsernameQuerier().queryForUsernameLink(
|
||||
link: link,
|
||||
fromViewController: rootViewController,
|
||||
tx: tx
|
||||
) { _, aci in
|
||||
SignalApp.shared.presentConversationForAddress(
|
||||
SignalServiceAddress(aci),
|
||||
animated: true
|
||||
)
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
SignalApp.shared.presentConversationForAddress(
|
||||
SignalServiceAddress(aci),
|
||||
animated: true,
|
||||
)
|
||||
}
|
||||
|
||||
case .stickerPack(let stickerPackInfo):
|
||||
|
||||
@@ -44,20 +44,19 @@ extension BaseMemberViewController: UsernameLinkScanDelegate {}
|
||||
|
||||
extension UsernameLinkScanDelegate where Self: RecipientPickerDelegate & RecipientPickerContainerViewController {
|
||||
func usernameLinkScanned(_ usernameLink: Usernames.UsernameLink) {
|
||||
dismiss(animated: true) {
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
MainActor.assumeIsolated {
|
||||
UsernameQuerier().queryForUsernameLink(
|
||||
link: usernameLink,
|
||||
fromViewController: self,
|
||||
tx: tx
|
||||
) { _, aci in
|
||||
self.recipientPicker(
|
||||
self.recipientPicker,
|
||||
didSelectRecipient: .for(address: SignalServiceAddress(aci))
|
||||
)
|
||||
}
|
||||
dismiss(animated: true) { [self] in
|
||||
Task { @MainActor in
|
||||
guard let (_, aci) = await UsernameQuerier().queryForUsernameLink(
|
||||
link: usernameLink,
|
||||
fromViewController: self,
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
recipientPicker(
|
||||
recipientPicker,
|
||||
didSelectRecipient: .for(address: SignalServiceAddress(aci))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,17 +595,18 @@ extension AppSettingsViewController: UsernameLinkScanDelegate {
|
||||
}
|
||||
|
||||
presentingViewController.dismiss(animated: true) {
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
UsernameQuerier().queryForUsernameLink(
|
||||
Task {
|
||||
guard let (_, aci) = await UsernameQuerier().queryForUsernameLink(
|
||||
link: usernameLink,
|
||||
fromViewController: presentingViewController,
|
||||
tx: tx
|
||||
) { _, aci in
|
||||
SignalApp.shared.presentConversationForAddress(
|
||||
SignalServiceAddress(aci),
|
||||
animated: true
|
||||
)
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
SignalApp.shared.presentConversationForAddress(
|
||||
SignalServiceAddress(aci),
|
||||
animated: true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1299,14 +1299,16 @@ extension PhotoCaptureViewController: QRCodeSampleBufferScannerDelegate {
|
||||
{
|
||||
qrCodeScanned = true
|
||||
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
UsernameQuerier().queryForUsernameLink(
|
||||
Task {
|
||||
guard let (username, aci) = await UsernameQuerier().queryForUsernameLink(
|
||||
link: usernameLink,
|
||||
fromViewController: self,
|
||||
tx: tx,
|
||||
failureSheetDismissalDelegate: self,
|
||||
onSuccess: self.showUsernameLinkSheet(username:aci:)
|
||||
)
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
showUsernameLinkSheet(username: username, aci: aci)
|
||||
}
|
||||
} else if
|
||||
let provisioningURL = DeviceProvisioningURL(urlString: qrCodeString),
|
||||
|
||||
@@ -15,12 +15,16 @@ public enum ActionSheetDisplayableError: Error {
|
||||
/// manually cancelled makes it obvious why a given action was aborted.
|
||||
case userCancelled
|
||||
|
||||
public func showActionSheet(from fromViewController: UIViewController?) {
|
||||
public func showActionSheet(
|
||||
from fromViewController: UIViewController?,
|
||||
dismissalDelegate: SheetDismissalDelegate? = nil,
|
||||
) {
|
||||
if let localizedActionSheetMessage {
|
||||
OWSActionSheets.showActionSheet(
|
||||
title: localizedActionSheetTitle,
|
||||
message: localizedActionSheetMessage,
|
||||
fromViewController: fromViewController,
|
||||
dismissalDelegate: dismissalDelegate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,18 +1430,15 @@ extension RecipientPickerViewController {
|
||||
}
|
||||
|
||||
private func findByUsername(_ username: String) {
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
UsernameQuerier().queryForUsername(
|
||||
Task {
|
||||
guard let aci = await UsernameQuerier().queryForUsername(
|
||||
username: username,
|
||||
fromViewController: self,
|
||||
tx: tx,
|
||||
onSuccess: { [weak self] aci in
|
||||
AssertIsOnMainThread()
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let self else { return }
|
||||
self.tryToSelectRecipient(.for(address: SignalServiceAddress(aci)))
|
||||
}
|
||||
)
|
||||
tryToSelectRecipient(.for(address: SignalServiceAddress(aci)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,20 +141,19 @@ public class FindByUsernameViewController: OWSTableViewController2 {
|
||||
private func didTapNext() {
|
||||
let usernameValue = self.usernameValue
|
||||
usernameTextField.resignFirstResponder()
|
||||
SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
UsernameQuerier().queryForUsername(
|
||||
|
||||
Task {
|
||||
guard let aci = await UsernameQuerier().queryForUsername(
|
||||
username: usernameValue,
|
||||
fromViewController: self,
|
||||
tx: tx,
|
||||
failureSheetDismissalDelegate: self,
|
||||
onSuccess: { [weak self] aci in
|
||||
AssertIsOnMainThread()
|
||||
self?.findByUsernameDelegate?.findByUsername(address: SignalServiceAddress(aci))
|
||||
}
|
||||
)
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
findByUsernameDelegate?.findByUsername(address: SignalServiceAddress(aci))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FindByUsernameViewController: SheetDismissalDelegate {
|
||||
|
||||
@@ -8,7 +8,7 @@ public import SignalServiceKit
|
||||
|
||||
public struct UsernameQuerier {
|
||||
private let contactsManager: any ContactManager
|
||||
private let databaseStorage: SDSDatabaseStorage
|
||||
private let db: DB
|
||||
private let localUsernameManager: LocalUsernameManager
|
||||
private let networkManager: NetworkManager
|
||||
private let profileManager: ProfileManager
|
||||
@@ -23,7 +23,7 @@ public struct UsernameQuerier {
|
||||
public init() {
|
||||
self.init(
|
||||
contactsManager: SSKEnvironment.shared.contactManagerRef,
|
||||
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
|
||||
db: DependenciesBridge.shared.db,
|
||||
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
|
||||
networkManager: SSKEnvironment.shared.networkManagerRef,
|
||||
profileManager: SSKEnvironment.shared.profileManagerRef,
|
||||
@@ -39,7 +39,7 @@ public struct UsernameQuerier {
|
||||
|
||||
public init(
|
||||
contactsManager: any ContactManager,
|
||||
databaseStorage: SDSDatabaseStorage,
|
||||
db: DB,
|
||||
localUsernameManager: LocalUsernameManager,
|
||||
networkManager: NetworkManager,
|
||||
profileManager: ProfileManager,
|
||||
@@ -52,7 +52,7 @@ public struct UsernameQuerier {
|
||||
usernameLookupManager: UsernameLookupManager
|
||||
) {
|
||||
self.contactsManager = contactsManager
|
||||
self.databaseStorage = databaseStorage
|
||||
self.db = db
|
||||
self.localUsernameManager = localUsernameManager
|
||||
self.networkManager = networkManager
|
||||
self.profileManager = profileManager
|
||||
@@ -65,144 +65,168 @@ public struct UsernameQuerier {
|
||||
self.usernameLookupManager = usernameLookupManager
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Query for the username via the given link, internally handling
|
||||
/// displaying errors as appropriate. Callers should do nothing if this
|
||||
/// method returns `nil`.
|
||||
@MainActor
|
||||
public func queryForUsernameLink(
|
||||
link: Usernames.UsernameLink,
|
||||
fromViewController: UIViewController,
|
||||
tx: DBReadTransaction,
|
||||
failureSheetDismissalDelegate: (any SheetDismissalDelegate)? = nil,
|
||||
onSuccess: @escaping (_ username: String, _ aci: Aci) -> Void
|
||||
) {
|
||||
let usernameState = localUsernameManager.usernameState(tx: tx)
|
||||
if
|
||||
let localAci = tsAccountManager.localIdentifiers(tx: tx)?.aci,
|
||||
let localLink = usernameState.usernameLink,
|
||||
let localUsername = usernameState.username,
|
||||
localLink == link
|
||||
{
|
||||
queryMatchedLocalUser(
|
||||
onSuccess: { onSuccess(localUsername, $0) },
|
||||
localAci: localAci,
|
||||
tx: tx
|
||||
)
|
||||
return
|
||||
failureSheetDismissalDelegate: SheetDismissalDelegate? = nil,
|
||||
) async -> (username: String, Aci)? {
|
||||
do throws(ActionSheetDisplayableError) {
|
||||
return try await _queryForUsernameLink(link: link, fromViewController: fromViewController)
|
||||
} catch {
|
||||
error.showActionSheet(from: fromViewController, dismissalDelegate: failureSheetDismissalDelegate)
|
||||
return nil
|
||||
}
|
||||
|
||||
ModalActivityIndicatorViewController.present(
|
||||
fromViewController: fromViewController,
|
||||
canCancel: true,
|
||||
asyncBlock: { modal in
|
||||
do {
|
||||
let username = try await usernameLinkManager.decryptEncryptedLink(link: link)
|
||||
guard let username else {
|
||||
modal.dismissIfNotCanceled {
|
||||
showUsernameLinkOutdatedError(dismissalDelegate: failureSheetDismissalDelegate)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let hashedUsername = try? Usernames.HashedUsername(
|
||||
forUsername: username
|
||||
) else {
|
||||
modal.dismissIfNotCanceled {
|
||||
showInvalidUsernameError(username: username, dismissalDelegate: failureSheetDismissalDelegate)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let usernameAci = try await queryServiceForUsername(hashedUsername: hashedUsername)
|
||||
modal.dismissIfNotCanceled {
|
||||
onSuccess(hashedUsername.usernameString, usernameAci)
|
||||
}
|
||||
} catch {
|
||||
modal.dismissIfNotCanceled {
|
||||
handleError(error, dismissalDelegate: failureSheetDismissalDelegate)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Query the service for the given username, invoking a callback if the
|
||||
/// username is successfully resolved to an ACI.
|
||||
///
|
||||
/// - Parameter onSuccess
|
||||
/// A callback invoked if the queried username resolves to an ACI.
|
||||
/// Guaranteed to be called on the main thread.
|
||||
private func _queryForUsernameLink(
|
||||
link: Usernames.UsernameLink,
|
||||
fromViewController: UIViewController,
|
||||
) async throws(ActionSheetDisplayableError) -> (username: String, Aci) {
|
||||
let (localAci, localLink, localUsername): (
|
||||
Aci?,
|
||||
Usernames.UsernameLink?,
|
||||
String?,
|
||||
) = db.read { tx in
|
||||
let usernameState = localUsernameManager.usernameState(tx: tx)
|
||||
return (
|
||||
tsAccountManager.localIdentifiers(tx: tx)?.aci,
|
||||
usernameState.usernameLink,
|
||||
usernameState.username,
|
||||
)
|
||||
}
|
||||
|
||||
if
|
||||
let localAci,
|
||||
let localLink,
|
||||
let localUsername,
|
||||
localLink == link
|
||||
{
|
||||
return (localUsername, localAci)
|
||||
}
|
||||
|
||||
return try await ModalActivityIndicatorViewController.presentAndPropagateResult(
|
||||
from: fromViewController,
|
||||
canCancel: true,
|
||||
) { () throws(ActionSheetDisplayableError) -> (username: String, Aci) in
|
||||
let username: String?
|
||||
do {
|
||||
username = try await usernameLinkManager.decryptEncryptedLink(link: link)
|
||||
} catch is CancellationError {
|
||||
throw .userCancelled
|
||||
} catch where error.isNetworkFailureOrTimeout {
|
||||
throw .networkError
|
||||
} catch {
|
||||
Logger.warn("Failed to decrypt username link with generic error! \(error)")
|
||||
throw .usernameLookupGenericError()
|
||||
}
|
||||
|
||||
guard let username else {
|
||||
throw .usernameLinkNoLongerValidError()
|
||||
}
|
||||
|
||||
guard let hashedUsername = try? Usernames.HashedUsername(
|
||||
forUsername: username
|
||||
) else {
|
||||
throw .usernameInvalidError(username)
|
||||
}
|
||||
|
||||
do {
|
||||
let usernameAci = try await queryServiceForUsername(hashedUsername: hashedUsername)
|
||||
return (username, usernameAci)
|
||||
} catch is CancellationError {
|
||||
throw .userCancelled
|
||||
} catch is UsernameNotFoundError {
|
||||
throw .usernameNotFoundError(username)
|
||||
} catch where error.isNetworkFailureOrTimeout {
|
||||
throw .networkError
|
||||
} catch {
|
||||
Logger.warn("Failed to look up username for link with generic error! \(error)")
|
||||
throw .usernameLookupGenericError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Query for the given username, internally handling displaying errors as
|
||||
/// appropriate. Callers should do nothing if this method returns `nil`.
|
||||
@MainActor
|
||||
public func queryForUsername(
|
||||
username: String,
|
||||
fromViewController: UIViewController,
|
||||
tx: DBReadTransaction,
|
||||
failureSheetDismissalDelegate: (any SheetDismissalDelegate)? = nil,
|
||||
onSuccess: @escaping (Aci) -> Void
|
||||
) {
|
||||
failureSheetDismissalDelegate: SheetDismissalDelegate? = nil,
|
||||
) async -> Aci? {
|
||||
do throws(ActionSheetDisplayableError) {
|
||||
return try await _queryForUsername(username: username, fromViewController: fromViewController)
|
||||
} catch {
|
||||
error.showActionSheet(from: fromViewController, dismissalDelegate: failureSheetDismissalDelegate)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func _queryForUsername(
|
||||
username: String,
|
||||
fromViewController: UIViewController,
|
||||
) async throws(ActionSheetDisplayableError) -> Aci {
|
||||
let (localAci, localUsername): (Aci?, String?) = db.read { tx in
|
||||
return (
|
||||
tsAccountManager.localIdentifiers(tx: tx)?.aci,
|
||||
localUsernameManager.usernameState(tx: tx).username,
|
||||
)
|
||||
}
|
||||
|
||||
if
|
||||
let localAci = tsAccountManager.localIdentifiers(tx: tx)?.aci,
|
||||
let localUsername = localUsernameManager.usernameState(tx: tx).username,
|
||||
let localAci,
|
||||
let localUsername,
|
||||
localUsername.caseInsensitiveCompare(username) == .orderedSame
|
||||
{
|
||||
queryMatchedLocalUser(onSuccess: onSuccess, localAci: localAci, tx: tx)
|
||||
return
|
||||
return localAci
|
||||
}
|
||||
|
||||
guard let hashedUsername = try? Usernames.HashedUsername(
|
||||
forUsername: username
|
||||
) else {
|
||||
showInvalidUsernameError(
|
||||
username: username,
|
||||
dismissalDelegate: failureSheetDismissalDelegate
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
ModalActivityIndicatorViewController.present(
|
||||
fromViewController: fromViewController,
|
||||
return try await ModalActivityIndicatorViewController.presentAndPropagateResult(
|
||||
from: fromViewController,
|
||||
canCancel: true,
|
||||
asyncBlock: { modal in
|
||||
do {
|
||||
let aci = try await queryServiceForUsername(hashedUsername: hashedUsername)
|
||||
modal.dismissIfNotCanceled {
|
||||
onSuccess(aci)
|
||||
}
|
||||
} catch {
|
||||
modal.dismissIfNotCanceled {
|
||||
handleError(error, dismissalDelegate: failureSheetDismissalDelegate)
|
||||
}
|
||||
}
|
||||
) { () throws(ActionSheetDisplayableError) -> Aci in
|
||||
guard let hashedUsername = try? Usernames.HashedUsername(
|
||||
forUsername: username
|
||||
) else {
|
||||
throw .usernameInvalidError(username)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle a query that we know will match the local user.
|
||||
///
|
||||
/// - Parameter tx
|
||||
/// An unused database transaction. Forced as a parameter here to draw
|
||||
/// attention to the fact that this workaround is required because the query
|
||||
/// methods are within the context of a transaction.
|
||||
private func queryMatchedLocalUser(
|
||||
onSuccess: @escaping (Aci) -> Void,
|
||||
localAci: Aci,
|
||||
tx _: DBReadTransaction
|
||||
) {
|
||||
// Dispatch asynchronously, since we are inside a transaction.
|
||||
DispatchQueue.main.async {
|
||||
onSuccess(localAci)
|
||||
do {
|
||||
return try await queryServiceForUsername(hashedUsername: hashedUsername)
|
||||
} catch is CancellationError {
|
||||
throw .userCancelled
|
||||
} catch is UsernameNotFoundError {
|
||||
throw .usernameNotFoundError(username)
|
||||
} catch where error.isNetworkFailureOrTimeout {
|
||||
throw .networkError
|
||||
} catch {
|
||||
Logger.warn("Failed to query username with generic error! \(error)")
|
||||
throw .usernameLookupGenericError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct UsernameNotFoundError: Error {
|
||||
var usernameString: String
|
||||
}
|
||||
// MARK: -
|
||||
|
||||
private struct UsernameNotFoundError: Error {}
|
||||
|
||||
/// Query the service for the ACI of the given username.
|
||||
private func queryServiceForUsername(hashedUsername: Usernames.HashedUsername) async throws -> Aci {
|
||||
let aci = try await self.usernameApiClient.lookupAci(forHashedUsername: hashedUsername)
|
||||
guard let aci else {
|
||||
throw UsernameNotFoundError(usernameString: hashedUsername.usernameString)
|
||||
throw UsernameNotFoundError()
|
||||
}
|
||||
await self.databaseStorage.awaitableWrite { tx in
|
||||
self.handleUsernameLookupCompleted(
|
||||
|
||||
await db.awaitableWrite { tx in
|
||||
handleUsernameLookupCompleted(
|
||||
aci: aci,
|
||||
username: hashedUsername.usernameString,
|
||||
tx: tx
|
||||
@@ -248,84 +272,57 @@ public struct UsernameQuerier {
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
// MARK: -
|
||||
|
||||
private func showInvalidUsernameError(
|
||||
username: String,
|
||||
dismissalDelegate: (any SheetDismissalDelegate)?
|
||||
) {
|
||||
OWSActionSheets.showActionSheet(
|
||||
title: OWSLocalizedString(
|
||||
private extension ActionSheetDisplayableError {
|
||||
static func usernameInvalidError(_ username: String) -> ActionSheetDisplayableError {
|
||||
return .custom(
|
||||
localizedTitle: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_INVALID_USERNAME_TITLE",
|
||||
comment: "Title for an action sheet indicating that a user-entered username value is not a valid username."
|
||||
),
|
||||
message: String(
|
||||
localizedMessage: String(
|
||||
format: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_INVALID_USERNAME_MESSAGE_FORMAT",
|
||||
comment: "A message indicating that a user-entered username value is not a valid username. Embeds {{ a username }}."
|
||||
),
|
||||
username
|
||||
),
|
||||
dismissalDelegate: dismissalDelegate
|
||||
username,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func showUsernameNotFoundError(
|
||||
username: String,
|
||||
dismissalDelegate: (any SheetDismissalDelegate)?
|
||||
) {
|
||||
OWSActionSheets.showActionSheet(
|
||||
title: OWSLocalizedString(
|
||||
static func usernameNotFoundError(_ username: String) -> ActionSheetDisplayableError {
|
||||
return .custom(
|
||||
localizedTitle: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_NOT_FOUND_TITLE",
|
||||
comment: "Title for an action sheet indicating that the given username is not associated with a registered Signal account."
|
||||
),
|
||||
message: String(
|
||||
localizedMessage: String(
|
||||
format: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_NOT_FOUND_MESSAGE_FORMAT",
|
||||
comment: "A message indicating that the given username is not associated with a registered Signal account. Embeds {{ a username }}."
|
||||
),
|
||||
username
|
||||
username,
|
||||
),
|
||||
dismissalDelegate: dismissalDelegate
|
||||
)
|
||||
}
|
||||
|
||||
private func showUsernameLinkOutdatedError(
|
||||
dismissalDelegate: (any SheetDismissalDelegate)?
|
||||
) {
|
||||
OWSActionSheets.showActionSheet(
|
||||
title: CommonStrings.errorAlertTitle,
|
||||
message: OWSLocalizedString(
|
||||
static func usernameLinkNoLongerValidError() -> ActionSheetDisplayableError {
|
||||
return .custom(
|
||||
localizedTitle: CommonStrings.errorAlertTitle,
|
||||
localizedMessage: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_LINK_NO_LONGER_VALID_MESSAGE",
|
||||
comment: "A message indicating that a username link the user attempted to query is no longer valid."
|
||||
),
|
||||
dismissalDelegate: dismissalDelegate
|
||||
)
|
||||
}
|
||||
|
||||
private func handleError(
|
||||
_ error: any Error,
|
||||
dismissalDelegate: (any SheetDismissalDelegate)?,
|
||||
) {
|
||||
if let notFoundError = error as? UsernameNotFoundError {
|
||||
showUsernameNotFoundError(username: notFoundError.usernameString, dismissalDelegate: dismissalDelegate)
|
||||
} else {
|
||||
showGenericError(dismissalDelegate: dismissalDelegate)
|
||||
}
|
||||
}
|
||||
|
||||
private func showGenericError(
|
||||
dismissalDelegate: (any SheetDismissalDelegate)?
|
||||
) {
|
||||
Logger.error("Error while querying for username!")
|
||||
|
||||
OWSActionSheets.showErrorAlert(
|
||||
message: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_ERROR_MESSAGE",
|
||||
comment: "A message indicating that username lookup failed."
|
||||
),
|
||||
dismissalDelegate: dismissalDelegate
|
||||
)
|
||||
static func usernameLookupGenericError() -> ActionSheetDisplayableError {
|
||||
return .custom(localizedMessage: OWSLocalizedString(
|
||||
"USERNAME_LOOKUP_ERROR_MESSAGE",
|
||||
comment: "A message indicating that username lookup failed."
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,13 +131,14 @@ public class ModalActivityIndicatorViewController: OWSViewController {
|
||||
@MainActor
|
||||
public class func presentAndPropagateResult<T, E>(
|
||||
from viewController: UIViewController,
|
||||
canCancel: Bool = false,
|
||||
presentationDelay: TimeInterval = Constants.defaultPresentationDelay,
|
||||
wrappedAsyncBlock: @escaping () async throws(E) -> T
|
||||
) async throws(E) -> T {
|
||||
let result: Result<T, E> = await withCheckedContinuation { continuation in
|
||||
present(
|
||||
fromViewController: viewController,
|
||||
canCancel: false,
|
||||
canCancel: canCancel,
|
||||
presentationDelay: presentationDelay,
|
||||
asyncBlock: { modal in
|
||||
let result = await Result(catching: wrappedAsyncBlock)
|
||||
|
||||
Reference in New Issue
Block a user