forked from shadowfacts/Tusker
Shadowfacts
c489d018bd
# Conflicts: # Tusker/Caching/ImageCache.swift # Tusker/Extensions/PKDrawing+Render.swift # Tusker/MultiThreadDictionary.swift # Tusker/Views/BaseEmojiLabel.swift
173 lines
6.0 KiB
Swift
173 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
|
|
#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)")
|
|
}
|