From 2444783edf3eb204bb8fb39d02f567a67dfa4425 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 29 Mar 2022 22:21:33 -0400 Subject: [PATCH] Add error reporter to Client.Error toast on long-press --- Pachyderm/Client.swift | 50 +++--- Pachyderm/Request/Method.swift | 4 +- Pachyderm/Utilities/InstanceSelector.swift | 11 +- Tusker.xcodeproj/project.pbxproj | 20 ++- Tusker/MainSceneDelegate.swift | 10 +- .../ConversationTableViewController.swift | 4 +- .../CrashReporterViewController.swift | 122 +++------------ .../IssueReporterViewController.swift | 145 ++++++++++++++++++ ...er.xib => IssueReporterViewController.xib} | 37 +++-- .../InstanceSelectorTableViewController.swift | 4 +- ...fableTimelineLikeTableViewController.swift | 6 +- Tusker/Views/Toast/ToastConfiguration.swift | 23 ++- Tusker/Views/Toast/ToastView.swift | 13 ++ 13 files changed, 286 insertions(+), 163 deletions(-) create mode 100644 Tusker/Screens/Crash Reporter/IssueReporterViewController.swift rename Tusker/Screens/Crash Reporter/{CrashReporterViewController.xib => IssueReporterViewController.xib} (84%) diff --git a/Pachyderm/Client.swift b/Pachyderm/Client.swift index 8218cfe148..9baf91d9e3 100644 --- a/Pachyderm/Client.swift +++ b/Pachyderm/Client.swift @@ -68,29 +68,32 @@ public class Client { @discardableResult public func run(_ request: Request, completion: @escaping Callback) -> URLSessionTask? { - guard let request = createURLRequest(request: request) else { - completion(.failure(Error.invalidRequest)) + guard let urlRequest = createURLRequest(request: request) else { + completion(.failure(Error(request: request, type: .invalidRequest))) return nil } - let task = session.dataTask(with: request) { data, response, error in + let task = session.dataTask(with: urlRequest) { data, response, error in if let error = error { - completion(.failure(.networkError(error))) + completion(.failure(Error(request: request, type: .networkError(error)))) return } guard let data = data, let response = response as? HTTPURLResponse else { - completion(.failure(.invalidResponse)) + completion(.failure(Error(request: request, type: .invalidResponse))) return } guard response.statusCode == 200 else { let mastodonError = try? Client.decoder.decode(MastodonError.self, from: data) - let error: Error = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode) - completion(.failure(error)) + let type: ErrorType = mastodonError.flatMap { .mastodonError($0.description) } ?? .unexpectedStatus(response.statusCode) + completion(.failure(Error(request: request, type: type))) return } - guard let result = try? Client.decoder.decode(Result.self, from: data) else { - completion(.failure(.invalidModel)) + let result: Result + do { + result = try Client.decoder.decode(Result.self, from: data) + } catch { + completion(.failure(Error(request: request, type: .invalidModel(error)))) return } let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init) @@ -392,16 +395,19 @@ public class Client { } extension Client { - public enum Error: LocalizedError { - case networkError(Swift.Error) - case unexpectedStatus(Int) - case invalidRequest - case invalidResponse - case invalidModel - case mastodonError(String) + public struct Error: LocalizedError { + public let requestMethod: Method + public let requestPath: String + public let type: ErrorType + + init(request: Request, type: ErrorType) { + self.requestMethod = request.method + self.requestPath = request.path + self.type = type + } public var localizedDescription: String { - switch self { + switch type { case .networkError(let error): return "Network Error: \(error.localizedDescription)" // todo: support more status codes @@ -413,11 +419,19 @@ extension Client { return "Invalid Request" case .invalidResponse: return "Invalid Response" - case .invalidModel: + case .invalidModel(_): return "Invalid Model" case .mastodonError(let error): return "Server Error: \(error)" } } } + public enum ErrorType: LocalizedError { + case networkError(Swift.Error) + case unexpectedStatus(Int) + case invalidRequest + case invalidResponse + case invalidModel(Swift.Error) + case mastodonError(String) + } } diff --git a/Pachyderm/Request/Method.swift b/Pachyderm/Request/Method.swift index 82285d8e73..60cad35672 100644 --- a/Pachyderm/Request/Method.swift +++ b/Pachyderm/Request/Method.swift @@ -8,12 +8,12 @@ import Foundation -enum Method { +public enum Method { case get, post, put, patch, delete } extension Method { - var name: String { + public var name: String { switch self { case .get: return "GET" diff --git a/Pachyderm/Utilities/InstanceSelector.swift b/Pachyderm/Utilities/InstanceSelector.swift index c45723ccb7..6ad7c2ff4d 100644 --- a/Pachyderm/Utilities/InstanceSelector.swift +++ b/Pachyderm/Utilities/InstanceSelector.swift @@ -12,7 +12,7 @@ public class InstanceSelector { private static let decoder = JSONDecoder() - public static func getInstances(category: String?, completion: @escaping Client.Callback<[Instance]>) { + public static func getInstances(category: String?, completion: @escaping (Result<[Instance], Client.ErrorType>) -> Void) { let url: URL if let category = category { url = URL(string: "https://api.joinmastodon.org/servers?category=\(category)")! @@ -34,11 +34,14 @@ public class InstanceSelector { completion(.failure(.unexpectedStatus(response.statusCode))) return } - guard let result = try? decoder.decode([Instance].self, from: data) else { - completion(.failure(Client.Error.invalidModel)) + let result: [Instance] + do { + result = try decoder.decode([Instance].self, from: data) + } catch { + completion(.failure(.invalidModel(error))) return } - completion(.success(result, nil)) + completion(.success(result)) } task.resume() } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index f301038cce..1594295b05 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0C214599E100432DC2 /* RequestRange.swift */; }; D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0E21459B6900432DC2 /* Pagination.swift */; }; D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; }; + D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */; }; D611C2CF232DC61100C86A49 /* HashtagTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */; }; D611C2D0232DC61100C86A49 /* HashtagTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */; }; D61AC1D3232E928600C54D2D /* InstanceSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61AC1D2232E928600C54D2D /* InstanceSelector.swift */; }; @@ -340,8 +341,8 @@ D6F0B12B24A3071C001E48C3 /* MainSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */; }; D6F0B17524A3A1AA001E48C3 /* MainSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */; }; D6F1F9DF27B0613300CB7D88 /* WebURL in Frameworks */ = {isa = PBXBuildFile; productRef = D6F1F9DE27B0613300CB7D88 /* WebURL */; settings = {ATTRIBUTES = (Required, ); }; }; - D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */; }; - D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */; }; + D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */; }; + D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */; }; /* End PBXBuildFile section */ @@ -480,6 +481,7 @@ D6109A0C214599E100432DC2 /* RequestRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRange.swift; sourceTree = ""; }; D6109A0E21459B6900432DC2 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; + D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = ""; }; D611C2CD232DC61100C86A49 /* HashtagTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTableViewCell.swift; sourceTree = ""; }; D611C2CE232DC61100C86A49 /* HashtagTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HashtagTableViewCell.xib; sourceTree = ""; }; D61AC1D2232E928600C54D2D /* InstanceSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceSelector.swift; sourceTree = ""; }; @@ -757,8 +759,8 @@ D6EE63FA2551F7F60065485C /* StatusCollapseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCollapseButton.swift; sourceTree = ""; }; D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitViewController.swift; sourceTree = ""; }; D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarViewController.swift; sourceTree = ""; }; - D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterViewController.swift; sourceTree = ""; }; - D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CrashReporterViewController.xib; sourceTree = ""; }; + D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueReporterViewController.swift; sourceTree = ""; }; + D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = IssueReporterViewController.xib; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1681,8 +1683,9 @@ D6F2E960249E772F005846BB /* Crash Reporter */ = { isa = PBXGroup; children = ( - D6F2E963249E8BFD005846BB /* CrashReporterViewController.swift */, - D6F2E964249E8BFD005846BB /* CrashReporterViewController.xib */, + D6114E0827F3EA3D0080E273 /* CrashReporterViewController.swift */, + D6F2E963249E8BFD005846BB /* IssueReporterViewController.swift */, + D6F2E964249E8BFD005846BB /* IssueReporterViewController.xib */, ); path = "Crash Reporter"; sourceTree = ""; @@ -1950,7 +1953,7 @@ D64BC19023C18B9D000D0238 /* FollowRequestNotificationTableViewCell.xib in Resources */, D6A3BC812321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.xib in Resources */, D667E5E12134937B0057A976 /* TimelineStatusTableViewCell.xib in Resources */, - D6F2E966249E8BFD005846BB /* CrashReporterViewController.xib in Resources */, + D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */, D662AEF0263A3B880082A153 /* PollFinishedTableViewCell.xib in Resources */, D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */, D6DEA0DF268400C300FE896A /* ConfirmLoadMoreTableViewCell.xib in Resources */, @@ -2144,7 +2147,7 @@ D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */, - D6F2E965249E8BFD005846BB /* CrashReporterViewController.swift in Sources */, + D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */, D6A57408255C53EC00674551 /* ComposeTextViewCaretScrolling.swift in Sources */, D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */, D6A3BC802321B7E600FD64D5 /* FollowNotificationGroupTableViewCell.swift in Sources */, @@ -2318,6 +2321,7 @@ D677284E24ECC01D00C732D3 /* Draft.swift in Sources */, D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */, D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */, + D6114E0927F3EA3D0080E273 /* CrashReporterViewController.swift in Sources */, D663625F2135C75500C9CBA2 /* ConversationMainStatusTableViewCell.swift in Sources */, D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */, D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */, diff --git a/Tusker/MainSceneDelegate.swift b/Tusker/MainSceneDelegate.swift index 30b7a3d103..166971dec9 100644 --- a/Tusker/MainSceneDelegate.swift +++ b/Tusker/MainSceneDelegate.swift @@ -127,7 +127,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate { return } - window!.rootViewController = CrashReporterViewController.create(report: report) + window!.rootViewController = CrashReporterViewController.create(report: report, delegate: self) #endif } @@ -208,10 +208,8 @@ extension MainSceneDelegate: OnboardingViewControllerDelegate { } } -extension MainSceneDelegate: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { - controller.dismiss(animated: true) { - self.showAppOrOnboardingUI() - } +extension MainSceneDelegate: IssueReporterViewControllerDelegate { + func didDismissReporter() { + showAppOrOnboardingUI() } } diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index ed77881c18..52b400928b 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -157,7 +157,7 @@ class ConversationTableViewController: EnhancedTableViewController { DispatchQueue.main.async { self.loadingState = .unloaded - let config = ToastConfiguration(from: error, with: "Error Loading Status") { [weak self] (toast) in + let config = ToastConfiguration(from: error, with: "Error Loading Status", in: self) { [weak self] (toast) in toast.dismissToast(animated: true) self?.loadMainStatus() } @@ -210,7 +210,7 @@ class ConversationTableViewController: EnhancedTableViewController { DispatchQueue.main.async { self.loadingState = .loadedMain - let config = ToastConfiguration(from: error, with: "Error Loading Content") { [weak self] (toast) in + let config = ToastConfiguration(from: error, with: "Error Loading Content", in: self) { [weak self] (toast) in toast.dismissToast(animated: true) self?.loadContext(for: mainStatus) } diff --git a/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift b/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift index 971fa2ee7a..c7383e7bf4 100644 --- a/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift +++ b/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift @@ -2,132 +2,46 @@ // CrashReporterViewController.swift // Tusker // -// Created by Shadowfacts on 6/20/20. -// Copyright © 2020 Shadowfacts. All rights reserved. +// Created by Shadowfacts on 3/29/22. +// Copyright © 2022 Shadowfacts. All rights reserved. // import UIKit import CrashReporter -import MessageUI - -class CrashReporterViewController: UIViewController { +class CrashReporterViewController: IssueReporterViewController { + private let report: PLCrashReport - private var reportText: String! - private var reportFilename: String { - let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp) - return "Tusker-crash-\(timestamp).crash" + override var preamble: String { + "Tusker has detected that it crashed the last time it was running. You can email the report to the developer or skip sending and continue to the app. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior to the crash that may be pertinent." } - @IBOutlet weak var crashReportTextView: UITextView! - @IBOutlet weak var sendReportButton: UIButton! - - static func create(report: PLCrashReport) -> UINavigationController { - let nav = UINavigationController(rootViewController: CrashReporterViewController(report: report)) - nav.navigationBar.prefersLargeTitles = true - return nav + override var subject: String { + "Tusker Crash Report" } - private init(report: PLCrashReport){ + static func create(report: PLCrashReport, delegate: IssueReporterViewControllerDelegate) -> UINavigationController { + return create(CrashReporterViewController(report: report, delegate: delegate)) + } + + private init(report: PLCrashReport, delegate: IssueReporterViewControllerDelegate) { self.report = report + let reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)! + let timestamp = ISO8601DateFormatter().string(from: report.systemInfo.timestamp) + let reportFilename = "Tusker-crash-\(timestamp).crash" - super.init(nibName: "CrashReporterViewController", bundle: .main) + super.init(reportText: reportText, reportFilename: reportFilename, delegate: delegate) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() navigationItem.title = NSLocalizedString("Crash Detected", comment: "crash reporter title") - navigationItem.largeTitleDisplayMode = .always - - crashReportTextView.font = .monospacedSystemFont(ofSize: 14, weight: .regular) - - reportText = PLCrashReportTextFormatter.stringValue(for: report, with: PLCrashReportTextFormatiOS)! - let info = "Tusker has detected that it crashed the last time it was running. You can email the report to the developer or skip sending and continue to the app. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior to the crash that may be pertinent.\n\n" - let attributed = NSMutableAttributedString() - attributed.append(NSAttributedString(string: info, attributes: [ - NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17), - NSAttributedString.Key.foregroundColor: UIColor.label - ])) - attributed.append(NSAttributedString(string: reportText, attributes: [ - NSAttributedString.Key.font: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular), - NSAttributedString.Key.foregroundColor: UIColor.label - ])) - crashReportTextView.attributedText = attributed - - sendReportButton.layer.cornerRadius = 12.5 - sendReportButton.layer.masksToBounds = true - - sendReportButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(sendReportButtonLongPressed))) } - private func updateSendReportButtonColor(lightened: Bool, animate: Bool) { - let color: UIColor - if lightened { - var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 - UIColor.systemBlue.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) - color = UIColor(hue: hue, saturation: 0.85 * saturation, brightness: brightness, alpha: alpha) - } else { - color = .systemBlue - } - if animate { - UIView.animate(withDuration: 0.25) { - self.sendReportButton.backgroundColor = color - } - } else { - sendReportButton.backgroundColor = color - } - } - - @IBAction func sendReportTouchDown(_ sender: Any) { - updateSendReportButtonColor(lightened: true, animate: false) - } - - @IBAction func sendReportButtonTouchDragExit(_ sender: Any) { - updateSendReportButtonColor(lightened: false, animate: true) - } - - @IBAction func sendReportButtonTouchDragEnter(_ sender: Any) { - updateSendReportButtonColor(lightened: true, animate: true) - } - - @IBAction func sendReportTouchUpInside(_ sender: Any) { - updateSendReportButtonColor(lightened: false, animate: true) - - let composeVC = MFMailComposeViewController() - composeVC.mailComposeDelegate = self - composeVC.setToRecipients(["me@shadowfacts.net"]) - composeVC.setSubject("Tusker Crash Report") - - let data = reportText.data(using: .utf8)! - composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename) - - self.present(composeVC, animated: true) - } - - @objc func sendReportButtonLongPressed() { - let dir = FileManager.default.temporaryDirectory - let url = dir.appendingPathComponent(reportFilename) - try! reportText.data(using: .utf8)!.write(to: url) - let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil) - present(activityController, animated: true) - } - - @IBAction func cancelPressed(_ sender: Any) { - (view.window!.windowScene!.delegate as! MainSceneDelegate).showAppOrOnboardingUI() - } - -} - -extension CrashReporterViewController: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { - controller.dismiss(animated: true) { - (self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAppOrOnboardingUI() - } - } } diff --git a/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift b/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift new file mode 100644 index 0000000000..41ac515e5f --- /dev/null +++ b/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift @@ -0,0 +1,145 @@ +// +// IssueReporterViewController.swift +// Tusker +// +// Created by Shadowfacts on 6/20/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import CrashReporter +import MessageUI + +protocol IssueReporterViewControllerDelegate: AnyObject { + func didDismissReporter() +} + +class IssueReporterViewController: UIViewController { + + static func create(_ self: IssueReporterViewController) -> UINavigationController { + let nav = UINavigationController(rootViewController: self) + nav.navigationBar.prefersLargeTitles = true + return nav + } + + static func create(reportText: String, reportFilename: String? = nil, delegate: IssueReporterViewControllerDelegate) -> UINavigationController { + let filename = reportFilename ?? "Tusker-error-\(ISO8601DateFormatter().string(from: Date())).txt" + return create(IssueReporterViewController(reportText: reportText, reportFilename: filename, delegate: delegate)) + } + + let reportText: String + let reportFilename: String + private weak var delegate: IssueReporterViewControllerDelegate? + + var preamble: String { + "Tusker has encountered an error. You can email a report to the developer. You may review the report below before sending.\n\nIf you choose to send the report, please include any additional details about what you were doing prior that may be pertinent." + } + var subject: String { + "Tusker Error Report" + } + + @IBOutlet weak var crashReportTextView: UITextView! + @IBOutlet weak var sendReportButton: UIButton! + + init(reportText: String, reportFilename: String, delegate: IssueReporterViewControllerDelegate?) { + self.reportText = reportText + self.reportFilename = reportFilename + self.delegate = delegate + super.init(nibName: "IssueReporterViewController", bundle: .main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = "Report an Error" + navigationItem.largeTitleDisplayMode = .always + + crashReportTextView.font = .monospacedSystemFont(ofSize: 14, weight: .regular) + + let attributed = NSMutableAttributedString() + attributed.append(NSAttributedString(string: preamble, attributes: [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17), + NSAttributedString.Key.foregroundColor: UIColor.label + ])) + attributed.append(NSAttributedString(string: "\n\n")) + attributed.append(NSAttributedString(string: reportText, attributes: [ + NSAttributedString.Key.font: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular), + NSAttributedString.Key.foregroundColor: UIColor.label + ])) + crashReportTextView.attributedText = attributed + + sendReportButton.layer.cornerRadius = 12.5 + sendReportButton.layer.masksToBounds = true + + sendReportButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(sendReportButtonLongPressed))) + } + + private func updateSendReportButtonColor(lightened: Bool, animate: Bool) { + let color: UIColor + if lightened { + var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 + UIColor.systemBlue.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + color = UIColor(hue: hue, saturation: 0.85 * saturation, brightness: brightness, alpha: alpha) + } else { + color = .systemBlue + } + if animate { + UIView.animate(withDuration: 0.25) { + self.sendReportButton.backgroundColor = color + } + } else { + sendReportButton.backgroundColor = color + } + } + + @IBAction func sendReportTouchDown(_ sender: Any) { + updateSendReportButtonColor(lightened: true, animate: false) + } + + @IBAction func sendReportButtonTouchDragExit(_ sender: Any) { + updateSendReportButtonColor(lightened: false, animate: true) + } + + @IBAction func sendReportButtonTouchDragEnter(_ sender: Any) { + updateSendReportButtonColor(lightened: true, animate: true) + } + + @IBAction func sendReportTouchUpInside(_ sender: Any) { + updateSendReportButtonColor(lightened: false, animate: true) + + let composeVC = MFMailComposeViewController() + composeVC.mailComposeDelegate = self + composeVC.setToRecipients(["me@shadowfacts.net"]) + composeVC.setSubject(subject) + + let data = reportText.data(using: .utf8)! + composeVC.addAttachmentData(data, mimeType: "text/plain", fileName: reportFilename) + + self.present(composeVC, animated: true) + } + + @objc func sendReportButtonLongPressed() { + let dir = FileManager.default.temporaryDirectory + let url = dir.appendingPathComponent(reportFilename) + try! reportText.data(using: .utf8)!.write(to: url) + let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil) + present(activityController, animated: true) + } + + @IBAction func cancelPressed(_ sender: Any) { + delegate?.didDismissReporter() + } + +} + +extension IssueReporterViewController: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true) { + self.delegate?.didDismissReporter() + } + } +} diff --git a/Tusker/Screens/Crash Reporter/CrashReporterViewController.xib b/Tusker/Screens/Crash Reporter/IssueReporterViewController.xib similarity index 84% rename from Tusker/Screens/Crash Reporter/CrashReporterViewController.xib rename to Tusker/Screens/Crash Reporter/IssueReporterViewController.xib index 76dfe814cb..198de5f97c 100644 --- a/Tusker/Screens/Crash Reporter/CrashReporterViewController.xib +++ b/Tusker/Screens/Crash Reporter/IssueReporterViewController.xib @@ -1,13 +1,15 @@ - + - + + + - + @@ -27,9 +29,9 @@ - + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - + @@ -43,13 +45,13 @@ - -