Tusker/Tusker/Views/Toast/ToastConfiguration.swift

166 lines
6.0 KiB
Swift

//
// ToastConfiguration.swift
// ToastConfiguration
//
// Created by Shadowfacts on 8/14/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import Sentry
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) {
let event = Event(error: error)
event.message = SentryMessage(formatted: "\(title): \(error)")
event.tags = [
"request_method": error.requestMethod.name,
"request_endpoint": error.requestEndpoint.description,
]
switch error.type {
case .invalidRequest:
event.tags!["error_type"] = "invalid_request"
case .invalidResponse:
event.tags!["error_type"] = "invalid_response"
case .invalidModel(let error):
event.tags!["error_type"] = "invalid_model"
event.extra = [
"underlying_error": String(describing: error)
]
case .mastodonError(let code, let error):
event.tags!["error_type"] = "mastodon_error"
event.tags!["response_code"] = "\(code)"
event.extra = [
"underlying_error": String(describing: error)
]
case .unexpectedStatus(let code):
event.tags!["error_type"] = "unexpected_status"
event.tags!["response_code"] = "\(code)"
default:
return
}
if let code = event.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):
event.tags!["instance_type"] = "mastodon"
event.tags!["mastodon_version"] = mastodonVersion?.description ?? "unknown"
switch mastodonType {
case .vanilla:
break
case .hometown(_):
event.tags!["mastodon_type"] = "hometown"
case .glitch:
event.tags!["mastodon_type"] = "glitch"
}
case .pleroma(let pleromaType):
event.tags!["instance_type"] = "pleroma"
switch pleromaType {
case .vanilla(let version):
event.tags!["pleroma_version"] = version?.description ?? "unknown"
case .akkoma(let version):
event.tags!["pleroma_type"] = "akkoma"
event.tags!["pleroma_version"] = version?.description ?? "unknown"
}
case .pixelfed:
event.tags!["instance_type"] = "pixelfed"
case .gotosocial:
event.tags!["instance_type"] = "gotosocial"
case .firefish(let calckeyVersion):
event.tags!["instance_type"] = "firefish"
if let calckeyVersion {
event.tags!["calckey_version"] = calckeyVersion
}
}
SentrySDK.capture(event: event)
toastErrorLogger.error("\(title, privacy: .public): \(error), \(event.tags!.debugDescription, privacy: .public)")
}