// // ToastConfiguration.swift // ToastConfiguration // // Created by Shadowfacts on 8/14/21. // Copyright © 2021 Shadowfacts. All rights reserved. // import UIKit import Pachyderm #if canImport(Sentry) import Sentry #endif import OSLog @_spi(InstanceType) import InstanceFeatures struct ToastConfiguration { var systemImageName: String? var titleFont: UIFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .boldSystemFont(ofSize: 14)) var title: String var subtitle: String? var actionTitle: String? var action: (@MainActor (ToastView) -> Void)? var longPressAction: (@MainActor (ToastView) -> Void)? var edgeSpacing: CGFloat = 8 var edge: Edge = .automatic var dismissOnScroll = true var dismissAutomaticallyAfter: TimeInterval? = nil var onAppear: ((UIViewPropertyAnimator?) -> Void)? var onDismiss: ((UIViewPropertyAnimator?) -> Void)? init(title: String) { self.title = title } enum Edge: Equatable { case top case bottom /// Determines edge based on the current device. Bottom on iPhone, top on iPad/Mac. case automatic } } extension ToastConfiguration { init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: (@MainActor (ToastView) -> Void)?) { self.init(title: title) // localizedDescription is statically dispatched, so we need to call it after the downcast if let error = error as? Pachyderm.Client.Error { self.subtitle = error.localizedDescription self.systemImageName = error.systemImageName self.longPressAction = { [unowned viewController] toast in toast.dismissToast(animated: true) let text = """ \(title): \(error.requestMethod.name) \(error.requestEndpoint) \(error.type) """ let reporter = IssueReporterViewController.create(reportText: text, dismiss: { [unowned viewController] in viewController.dismiss(animated: true) }) viewController.present(reporter, animated: true) } // TODO: this is a bizarre place to do this, but code path covers basically all errors captureError(error, in: viewController.apiController, title: title) } else { self.subtitle = error.localizedDescription self.systemImageName = "exclamationmark.triangle" } if let retryAction = retryAction { self.actionTitle = "Retry" self.action = retryAction } } init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: @Sendable @escaping @MainActor (ToastView) async -> Void) { self.init(from: error, with: title, in: viewController) { toast in Task { await retryAction(toast) } } } } fileprivate extension Pachyderm.Client.Error { var systemImageName: String { switch type { case .networkError(_): return "wifi.exclamationmark" case .unexpectedStatus(429): return "clock.badge.exclamationmark" default: return "exclamationmark.triangle" } } } private let toastErrorLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ToastError") private func captureError(_ error: Client.Error, in mastodonController: MastodonController, title: String) { var tags = [ "request_method": error.requestMethod.name, "request_endpoint": error.requestEndpoint.description, ] var extra: [String: String]? switch error.type { case .invalidRequest: tags["error_type"] = "invalid_request" case .invalidResponse: tags["error_type"] = "invalid_response" case .invalidModel(let error): tags["error_type"] = "invalid_model" extra = [ "underlying_error": String(describing: error) ] case .mastodonError(let code, let error): tags["error_type"] = "mastodon_error" tags["response_code"] = "\(code)" extra = [ "underlying_error": String(describing: error) ] case .unexpectedStatus(let code): tags["error_type"] = "unexpected_status" tags["response_code"] = "\(code)" default: return } if let code = tags["response_code"], code == "401" || code == "403" || code == "404" || code == "422" || code == "500" || code == "502" || code == "503" { return } switch mastodonController.instanceFeatures.instanceType { case .mastodon(let mastodonType, let mastodonVersion): tags["instance_type"] = "mastodon" tags["mastodon_version"] = mastodonVersion?.description ?? "unknown" switch mastodonType { case .vanilla: break case .hometown(_): tags["mastodon_type"] = "hometown" case .glitch: tags["mastodon_type"] = "glitch" } case .pleroma(let pleromaType): tags["instance_type"] = "pleroma" switch pleromaType { case .vanilla(let version): tags["pleroma_version"] = version?.description ?? "unknown" case .akkoma(let version): tags["pleroma_type"] = "akkoma" tags["pleroma_version"] = version?.description ?? "unknown" } case .pixelfed: tags["instance_type"] = "pixelfed" case .gotosocial: tags["instance_type"] = "gotosocial" case .firefish(let calckeyVersion): tags["instance_type"] = "firefish" if let calckeyVersion { tags["calckey_version"] = calckeyVersion } } #if canImport(Sentry) let event = Event(error: error) event.message = SentryMessage(formatted: "\(title): \(error)") event.tags = tags event.extra = extra SentrySDK.capture(event: event) #endif toastErrorLogger.error("\(title, privacy: .public): \(error), \(tags.debugDescription, privacy: .public)") }