Implement error handling.

This commit is contained in:
Marcin Czachursk 2023-01-15 12:41:55 +01:00
parent 3acdb018f6
commit e52302a0ce
32 changed files with 245 additions and 143 deletions

View File

@ -14,7 +14,7 @@ extension NetworkError: LocalizedError {
public var errorDescription: String? {
switch self {
case .notSuccessResponse(let response):
let statusCode = (response as? HTTPURLResponse)?.status
let statusCode = response.statusCode()
return NSLocalizedString("Network request returned not success status code: '\(statusCode?.localizedDescription ?? "unknown")'.", comment: "It's error returned from remote server.")
}
}

View File

@ -0,0 +1,14 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
public extension URLResponse {
func statusCode() -> HTTPStatusCode? {
let statusCode = (self as? HTTPURLResponse)?.status
return statusCode
}
}

View File

@ -14,12 +14,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode(Account.self, from: data)
return try await downloadJson(Account.self, request: request)
}
func getRelationship(for accountId: String) async throws -> Relationship? {
@ -28,13 +23,8 @@ public extension MastodonClientAuthenticated {
target: Mastodon.Account.relationships([accountId]),
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
let relationships = try JSONDecoder().decode([Relationship].self, from: data)
let relationships = try await downloadJson([Relationship].self, request: request)
return relationships.first
}
@ -51,12 +41,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode([Status].self, from: data)
return try await downloadJson([Status].self, request: request)
}
func follow(for accountId: String) async throws -> Relationship {
@ -66,12 +51,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode(Relationship.self, from: data)
return try await downloadJson(Relationship.self, request: request)
}
func unfollow(for accountId: String) async throws -> Relationship {
@ -81,12 +61,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode(Relationship.self, from: data)
return try await downloadJson(Relationship.self, request: request)
}
func getFollowers(for accountId: String, page: Int = 1) async throws -> [Account] {
@ -96,12 +71,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode([Account].self, from: data)
return try await downloadJson([Account].self, request: request)
}
func getFollowing(for accountId: String, page: Int = 1) async throws -> [Account] {
@ -110,12 +80,7 @@ public extension MastodonClientAuthenticated {
target: Mastodon.Account.following(accountId, nil, nil, nil, nil, page),
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode([Account].self, from: data)
return try await downloadJson([Account].self, request: request)
}
}

View File

@ -8,8 +8,6 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Account.self, from: data)
return try await downloadJson(Account.self, request: request)
}
}

View File

@ -14,11 +14,6 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode(Context.self, from: data)
return try await downloadJson(Context.self, request: request)
}
}

View File

@ -17,9 +17,7 @@ public extension MastodonClient {
)
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(App.self, from: data)
return try await downloadJson(App.self, request: request)
}
func authenticate(app: App, scope: Scopes) async throws -> OAuthSwiftCredential { // todo: we should not load OAuthSwift objects here
@ -65,9 +63,7 @@ public extension MastodonClient {
target: Mastodon.OAuth.authenticate(app, username, password, scope.asScopeString)
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(AccessToken.self, from: data)
return try await downloadJson(AccessToken.self, request: request)
}
}

View File

@ -2,9 +2,7 @@ import Foundation
public extension MastodonClient {
func readInstanceInformation() async throws -> Instance {
let request = try Self.request(for: baseURL, target: Mastodon.Instances.instance )
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Instance.self, from: data)
let request = try Self.request(for: baseURL, target: Mastodon.Instances.instance)
return try await downloadJson(Instance.self, request: request)
}
}

View File

@ -6,10 +6,8 @@ public extension MastodonClientAuthenticated {
for: baseURL,
target: Mastodon.Statuses.status(statusId),
withBearerToken: token)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func boost(statusId: StatusId) async throws -> Status {
@ -19,10 +17,8 @@ public extension MastodonClientAuthenticated {
target: Mastodon.Statuses.reblog(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func unboost(statusId: StatusId) async throws -> Status {
@ -32,9 +28,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func bookmark(statusId: StatusId) async throws -> Status {
@ -44,9 +38,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func unbookmark(statusId: StatusId) async throws -> Status {
@ -56,9 +48,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func favourite(statusId: StatusId) async throws -> Status {
@ -68,9 +58,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func unfavourite(statusId: StatusId) async throws -> Status {
@ -80,9 +68,7 @@ public extension MastodonClientAuthenticated {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
func new(statusComponents: Mastodon.Statuses.Components) async throws -> Status {
@ -91,8 +77,6 @@ public extension MastodonClientAuthenticated {
target: Mastodon.Statuses.new(statusComponents),
withBearerToken: token)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
return try await downloadJson(Status.self, request: request)
}
}

View File

@ -61,6 +61,15 @@ public class MastodonClient: MastodonClientProtocol {
oAuthContinuation?.resume(throwing: MastodonClientError.oAuthCancelled)
oAuthHandle?.cancel()
}
public func downloadJson<T>(_ type: T.Type, request: URLRequest) async throws -> T where T: Decodable {
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode(type, from: data)
}
}
public class MastodonClientAuthenticated: MastodonClientProtocol {
@ -86,10 +95,8 @@ public class MastodonClientAuthenticated: MastodonClientProtocol {
target: Mastodon.Timelines.home(maxId, sinceId, minId, limit),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode([Status].self, from: data)
return try await downloadJson([Status].self, request: request)
}
public func getPublicTimeline(isLocal: Bool = false,
@ -102,9 +109,8 @@ public class MastodonClientAuthenticated: MastodonClientProtocol {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode([Status].self, from: data)
return try await downloadJson([Status].self, request: request)
}
public func getTagTimeline(tag: String,
@ -118,9 +124,7 @@ public class MastodonClientAuthenticated: MastodonClientProtocol {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode([Status].self, from: data)
return try await downloadJson([Status].self, request: request)
}
public func saveMarkers(_ markers: [Mastodon.Markers.Timeline: StatusId]) async throws -> Markers {
@ -130,9 +134,7 @@ public class MastodonClientAuthenticated: MastodonClientProtocol {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Markers.self, from: data)
return try await downloadJson(Markers.self, request: request)
}
public func readMarkers(_ markers: Set<Mastodon.Markers.Timeline>) async throws -> Markers {
@ -142,8 +144,15 @@ public class MastodonClientAuthenticated: MastodonClientProtocol {
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Markers.self, from: data)
return try await downloadJson(Markers.self, request: request)
}
public func downloadJson<T>(_ type: T.Type, request: URLRequest) async throws -> T where T: Decodable {
let (data, response) = try await urlSession.data(for: request)
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
throw NetworkError.notSuccessResponse(response)
}
return try JSONDecoder().decode(type, from: data)
}
}

View File

@ -39,6 +39,7 @@
F85DBF8F296732E20069BF89 /* FollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DBF8E296732E20069BF89 /* FollowersView.swift */; };
F85DBF912967385F0069BF89 /* FollowingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DBF902967385F0069BF89 /* FollowingView.swift */; };
F85DBF93296760790069BF89 /* CacheAvatarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85DBF92296760790069BF89 /* CacheAvatarService.swift */; };
F85E1320297409CD006A051D /* ErrorsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E131F297409CD006A051D /* ErrorsService.swift */; };
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
@ -92,6 +93,8 @@
F89D6C4C297197FE001DA3D4 /* ImageViewerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89D6C4B297197FE001DA3D4 /* ImageViewerViewModel.swift */; };
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
F8B1E64F2973F61400EE0D10 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = F8B1E64E2973F61400EE0D10 /* Drops */; };
F8B1E6512973FB7E00EE0D10 /* ToastrService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B1E6502973FB7E00EE0D10 /* ToastrService.swift */; };
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14393296AF21B001FE31D /* Double+Round.swift */; };
F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CC95CD2970761D00C9C2AC /* TintColor.swift */; };
@ -127,6 +130,7 @@
F85DBF8E296732E20069BF89 /* FollowersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersView.swift; sourceTree = "<group>"; };
F85DBF902967385F0069BF89 /* FollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingView.swift; sourceTree = "<group>"; };
F85DBF92296760790069BF89 /* CacheAvatarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheAvatarService.swift; sourceTree = "<group>"; };
F85E131F297409CD006A051D /* ErrorsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorsService.swift; sourceTree = "<group>"; };
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataClass.swift"; sourceTree = "<group>"; };
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataProperties.swift"; sourceTree = "<group>"; };
F866F6A229604161002E8F88 /* AccountDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataHandler.swift; sourceTree = "<group>"; };
@ -185,6 +189,7 @@
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
F8AF2A61297073FE00D2DA3F /* Vernissage20230112-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage20230112-001.xcdatamodel"; sourceTree = "<group>"; };
F8B1E6502973FB7E00EE0D10 /* ToastrService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastrService.swift; sourceTree = "<group>"; };
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
F8C14393296AF21B001FE31D /* Double+Round.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Round.swift"; sourceTree = "<group>"; };
F8CC95CD2970761D00C9C2AC /* TintColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TintColor.swift; sourceTree = "<group>"; };
@ -199,6 +204,7 @@
F8210DD52966BB7E001D9973 /* Nuke in Frameworks */,
F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */,
F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */,
F8B1E64F2973F61400EE0D10 /* Drops in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -392,6 +398,8 @@
F8A93D7F2965FED4001D8331 /* AccountService.swift */,
F8210DE02966D0C4001D9973 /* StatusService.swift */,
F85DBF92296760790069BF89 /* CacheAvatarService.swift */,
F8B1E6502973FB7E00EE0D10 /* ToastrService.swift */,
F85E131F297409CD006A051D /* ErrorsService.swift */,
);
path = Services;
sourceTree = "<group>";
@ -472,6 +480,7 @@
F8210DD62966BB7E001D9973 /* NukeExtensions */,
F8210DD82966BB7E001D9973 /* NukeUI */,
F89992C6296D3DF8005994BF /* MastodonKit */,
F8B1E64E2973F61400EE0D10 /* Drops */,
);
productName = Vernissage;
productReference = F88C2468295C37B80006098B /* Vernissage.app */;
@ -503,6 +512,7 @@
mainGroup = F88C245F295C37B80006098B;
packageReferences = (
F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */,
F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */,
);
productRefGroup = F88C2469295C37B80006098B /* Products */;
projectDirPath = "";
@ -565,6 +575,7 @@
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F86B7223296C4BF500EE59EC /* ContentWarning.swift in Sources */,
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */,
F8B1E6512973FB7E00EE0D10 /* ToastrService.swift in Sources */,
F85DBF912967385F0069BF89 /* FollowingView.swift in Sources */,
F89992CE296D92E7005994BF /* AttachmentViewModel.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
@ -593,6 +604,7 @@
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */,
F85E1320297409CD006A051D /* ErrorsService.swift in Sources */,
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */,
@ -832,6 +844,14 @@
minimumVersion = 11.5.3;
};
};
F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/omaralbeik/Drops";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -854,6 +874,11 @@
isa = XCSwiftPackageProductDependency;
productName = MastodonKit;
};
F8B1E64E2973F61400EE0D10 /* Drops */ = {
isa = XCSwiftPackageProductDependency;
package = F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */;
productName = Drops;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */

View File

@ -18,7 +18,7 @@ class AccountDataHandler {
do {
return try context.fetch(fetchRequest)
} catch {
print("Error during fetching accounts")
ErrorService.shared.handle(error, message: "Accounts cannot be retrieved (getAccountsData).")
return []
}
}
@ -48,7 +48,7 @@ class AccountDataHandler {
do {
return try context.fetch(fetchRequest).first
} catch {
print("Error during fetching status (getAccountData)")
ErrorService.shared.handle(error, message: "Error during fetching status (getAccountData).")
return nil
}
}

View File

@ -19,7 +19,7 @@ class ApplicationSettingsHandler {
do {
settingsList = try context.fetch(fetchRequest)
} catch {
print("Error during fetching application settings")
ErrorService.shared.handle(error, message: "Error during fetching application settings.")
}
if let settings = settingsList.first {

View File

@ -26,7 +26,7 @@ class StatusDataHandler {
do {
return try context.fetch(fetchRequest).first
} catch {
print("Error during fetching status (getStatusData)")
ErrorService.shared.handle(error, message: "Error during fetching status (getStatusData).")
return nil
}
}
@ -45,7 +45,7 @@ class StatusDataHandler {
let statuses = try context.fetch(fetchRequest)
return statuses.first
} catch {
print("Error during fetching maximum status (getMaximumStatus)")
ErrorService.shared.handle(error, message: "Error during fetching maximum status (getMaximumStatus).")
return nil
}
}
@ -64,11 +64,27 @@ class StatusDataHandler {
let statuses = try context.fetch(fetchRequest)
return statuses.first
} catch {
print("Error during fetching minimum status (getMinimumtatus)")
ErrorService.shared.handle(error, message: "Error during fetching minimum status (getMinimumtatus).")
return nil
}
}
func remove(accountId: String, statusId: String) {
let status = self.getStatusData(accountId: accountId, statusId: statusId)
guard let status else {
return
}
let context = CoreDataHandler.shared.container.viewContext
context.delete(status)
do {
try context.save()
} catch {
ErrorService.shared.handle(error, message: "Error during deleting status (remove).")
}
}
func createStatusDataEntity(viewContext: NSManagedObjectContext? = nil) -> StatusData {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return StatusData(context: context)

View File

@ -24,3 +24,10 @@ extension Color {
static let accentColor9 = Color("AccentColor9")
static let accentColor10 = Color("AccentColor10")
}
extension Color {
func toUIColor() -> UIColor {
UIColor(self)
}
}

View File

@ -32,7 +32,7 @@ public final class HapticService: ObservableObject {
try player?.start(atTime: CHHapticTimeImmediate)
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Haptic service failed.")
}
}

View File

@ -34,4 +34,8 @@ public enum TintColor: Int {
return Color.accentColor10
}
}
public func uiColor() -> UIColor {
return self.color().toUIColor()
}
}

View File

@ -32,8 +32,7 @@ public class AuthorizationService {
try await self.refreshCredentials(accountData: accountData)
result(accountData)
} catch {
// TODO: show information to the user.
print("Cannot refresh credentials!!!")
ErrorService.shared.handle(error, message: "Issues during refreshing credentials.", showToastr: true)
}
}
}
@ -94,7 +93,7 @@ public class AuthorizationService {
accountData.avatarData = avatarData
}
catch {
print("Avatar has not been downloaded")
ErrorService.shared.handle(error, message: "Avatar has not been downloaded.")
}
}
@ -151,7 +150,7 @@ public class AuthorizationService {
accountData.avatarData = avatarData
}
catch {
print("Avatar has not been downloaded")
ErrorService.shared.handle(error, message: "Avatar has not been downloaded.")
}
}

View File

@ -35,7 +35,7 @@ public class CacheAvatarService {
CacheAvatarService.shared.addImage(for: accountId, data: avatarData)
}
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Downloading avatar into cache failed.")
}
}

View File

@ -0,0 +1,20 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
public class ErrorService {
public static let shared = ErrorService()
private init() { }
public func handle(_ error: Error, message: String, showToastr: Bool = false) {
if showToastr {
ToastrService.shared.showError(subtitle: message)
}
print("Error: \(error.localizedDescription)")
}
}

View File

@ -11,6 +11,15 @@ public class StatusService {
public static let shared = StatusService()
private init() { }
public func getStatus(withId statusId: String, and accountData: AccountData?) async throws -> Status? {
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
return nil
}
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
return try await client.read(statusId: statusId)
}
func favourite(statusId: String, accountData: AccountData?) async throws -> Status? {
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
return nil

View File

@ -40,15 +40,6 @@ public class TimelineService {
return try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id)
}
public func getStatus(withId statusId: String, and accountData: AccountData?) async throws -> Status? {
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
return nil
}
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
return try await client.read(statusId: statusId)
}
public func getComments(for statusId: String, and accountData: AccountData) async throws -> [CommentViewModel] {
var commentViewModels: [CommentViewModel] = []
@ -196,7 +187,7 @@ public class TimelineService {
return (attachmentUrl.key, nil)
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Fatching all images failed.")
return (attachmentUrl.key, nil)
}
}

View File

@ -0,0 +1,54 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
import SwiftUI
import Drops
public class ToastrService {
public static let shared = ToastrService()
private init() { }
public func showSuccess(_ title: String, imageSystemName: String, subtitle: String? = nil) {
let drop = Drop(
title: title,
subtitle: subtitle,
icon: self.createImage(systemName: imageSystemName, color: ApplicationState.shared.tintColor.uiColor()),
action: .init {
Drops.hideCurrent()
},
position: .top,
duration: 2.0,
accessibility: ""
)
Drops.show(drop)
}
public func showError(title: String = "Unexpected error", imageSystemName: String = "ant.circle.fill", subtitle: String? = nil) {
let drop = Drop(
title: "Unexpected error",
subtitle: subtitle,
icon: self.createImage(systemName: imageSystemName, color: Color.red.toUIColor()),
action: .init {
Drops.hideCurrent()
},
position: .top,
duration: 2.0,
accessibility: ""
)
Drops.show(drop)
}
private func createImage(systemName: String, color: UIColor) -> UIImage? {
guard let uiImage = UIImage(systemName: systemName) else {
return nil
}
return uiImage.withTintColor(color, renderingMode: .alwaysOriginal)
}
}

View File

@ -89,6 +89,7 @@ struct ComposeView: View {
Task {
await self.publishStatus()
dismiss()
ToastrService.shared.showSuccess("Status published", imageSystemName: "message.fill")
}
} label: {
Text("Publish")
@ -115,7 +116,7 @@ struct ComposeView: View {
status: Mastodon.Statuses.Components(inReplyToId: self.statusViewModel?.id, text: self.text),
accountData: self.applicationState.accountData)
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Error during post status.", showToastr: true)
}
}

View File

@ -81,7 +81,7 @@ struct FollowersView: View {
await self.downloadAvatars(accounts: accountsFromApi)
self.accounts.append(contentsOf: accountsFromApi)
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Error during download followers from server.", showToastr: true)
}
}

View File

@ -81,7 +81,7 @@ struct FollowingView: View {
await self.downloadAvatars(accounts: accountsFromApi)
self.accounts.append(contentsOf: accountsFromApi)
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Error during download following from server.", showToastr: true)
}
}

View File

@ -49,7 +49,7 @@ struct HomeFeedView: View {
}
}
} catch {
print("Error", error)
ErrorService.shared.handle(error, message: "Error during download statuses from server.", showToastr: true)
}
}
}
@ -95,7 +95,7 @@ struct HomeFeedView: View {
_ = try await TimelineService.shared.onTopOfList(for: accountData)
}
} catch {
print("Error", error)
ErrorService.shared.handle(error, message: "Error during download statuses from server.", showToastr: true)
}
}
}

View File

@ -10,6 +10,8 @@ import AVFoundation
struct StatusView: View {
@EnvironmentObject var applicationState: ApplicationState
@Environment(\.dismiss) private var dismiss
@State var statusId: String
@State var imageBlurhash: String?
@State var imageWidth: Int32?
@ -116,7 +118,7 @@ struct StatusView: View {
}
// Get status from API.
if let status = try await TimelineService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
if let status = try await StatusService.shared.getStatus(withId: self.statusId, and: self.applicationState.accountData) {
let statusViewModel = StatusViewModel(status: status)
// Download images and recalculate exif data.
@ -137,8 +139,15 @@ struct StatusView: View {
_ = try await TimelineService.shared.updateStatus(statusDataFromDatabase, accountData: accountData, basedOn: status)
}
}
} catch {
print("Error \(error.localizedDescription)")
} catch NetworkError.notSuccessResponse(let response) {
if response.statusCode() == HTTPStatusCode.notFound, let accountId = self.applicationState.accountData?.id {
StatusDataHandler.shared.remove(accountId: accountId, statusId: self.statusId)
ErrorService.shared.handle(NetworkError.notSuccessResponse(response), message: "Status not existing anymore.", showToastr: true)
dismiss()
}
}
catch {
ErrorService.shared.handle(error, message: "Error during download status from server.", showToastr: true)
}
}
}

View File

@ -35,7 +35,7 @@ struct UserProfileView: View {
do {
try await self.loadData()
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Error during download account from server.", showToastr: true)
}
}
}

View File

@ -6,6 +6,7 @@
import SwiftUI
import MastodonKit
import Drops
struct InteractionRow: View {
@EnvironmentObject var applicationState: ApplicationState
@ -48,8 +49,10 @@ struct InteractionRow: View {
self.reblogged = status.reblogged
}
ToastrService.shared.showSuccess("Reblogged", imageSystemName: "paperplane.fill")
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Reblog action failed.", showToastr: true)
}
} label: {
HStack(alignment: .center) {
@ -74,8 +77,10 @@ struct InteractionRow: View {
self.favourited = status.favourited
}
ToastrService.shared.showSuccess("Favourited", imageSystemName: "hand.thumbsup.fill")
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Favourite action failed.", showToastr: true)
}
} label: {
HStack(alignment: .center) {
@ -95,8 +100,10 @@ struct InteractionRow: View {
self.bookmarked.toggle()
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Favourite action failed.", showToastr: true)
}
ToastrService.shared.showSuccess("Bookmarked", imageSystemName: "bookmark.fill")
} label: {
Image(systemName: self.bookmarked ? "bookmark.fill" : "bookmark")
}
@ -105,6 +112,7 @@ struct InteractionRow: View {
ActionButton {
// TODO: Share.
ToastrService.shared.showError(subtitle: "Sending new status failed!")
} label: {
Image(systemName: "square.and.arrow.up")
}

View File

@ -59,7 +59,7 @@ struct CommentsSection: View {
self.commentViewModels = try await TimelineService.shared.getComments(for: statusId, and: accountData)
}
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Comments cannot be downloaded.", showToastr: true)
}
}
}

View File

@ -115,7 +115,7 @@ struct UserProfileHeader: View {
}
}
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Relationship action failed.", showToastr: true)
}
}
}

View File

@ -39,7 +39,7 @@ struct UserProfileStatuses: View {
do {
try await self.loadMoreStatuses()
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Loading more statuses failed.", showToastr: true)
}
}
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
@ -53,7 +53,7 @@ struct UserProfileStatuses: View {
do {
try await self.loadStatuses()
} catch {
print("Error \(error.localizedDescription)")
ErrorService.shared.handle(error, message: "Loading statuses failed.", showToastr: true)
}
}
}